diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..91906b0
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,42 @@
+version: 2
+jobs:
+ build:
+ docker:
+ - image: circleci/android:api-28-alpha
+
+ environment:
+ JVM_OPTS: -Xmx3200m
+
+ steps:
+ - checkout:
+
+ - restore_cache:
+ key: jars-{{ checksum "Droidcon-Boston/build.gradle" }}-{{ checksum "Droidcon-Boston/app/build.gradle" }}
+
+ - run:
+ name: Chmod permissions #if permission for Gradlew Dependencies fail, use this.
+ command: cd Droidcon-Boston && pwd && ls && sudo chmod +x ./gradlew
+
+ - run:
+ name: Download Dependencies
+ command: cd Droidcon-Boston && ./gradlew androidDependencies
+
+ - save_cache:
+ paths:
+ - .gradle
+ key: jars-{{ checksum "Droidcon-Boston/build.gradle" }}-{{ checksum "Droidcon-Boston/app/build.gradle" }}
+
+ - run:
+ name: Static Analysis Checks
+ command: cd Droidcon-Boston && ./gradlew detekt ktlintCheck lint
+
+ - run:
+ name: Run Tests
+ command: cd Droidcon-Boston && ./gradlew test
+
+ - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
+ path: Droidcon-Boston/app/build/reports
+ destination: reports
+
+ - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
+ path: Droidcon-Boston/app/build/test-results
\ No newline at end of file
diff --git a/Droidcon-Boston/.idea/codeStyleSettings.xml b/Droidcon-Boston/.idea/codeStyleSettings.xml
deleted file mode 100644
index 61ac0f6..0000000
--- a/Droidcon-Boston/.idea/codeStyleSettings.xml
+++ /dev/null
@@ -1,778 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
-
-
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
-
- onAttach
-
-
-
-
-
-
-
-
-
- onCreate
-
-
-
-
-
-
-
-
-
- onCreateView
-
-
-
-
-
-
-
-
-
- onViewCreated
-
-
-
-
-
-
-
-
-
- onActivityCreated
-
-
-
-
-
-
-
-
-
- onViewStateRestored
-
-
-
-
-
-
-
-
-
- onStart
-
-
-
-
-
-
-
-
-
- onActivityResult
-
-
-
-
-
-
-
-
-
- onRestoreInstanceState
-
-
-
-
-
-
-
-
-
- onPostCreate
-
-
-
-
-
-
-
-
-
- onRestart
-
-
-
-
-
-
-
-
-
- onResume
-
-
-
-
-
-
-
-
-
- onPostResume
-
-
-
-
-
-
-
-
-
- onNewIntent
-
-
-
-
-
-
-
-
-
- onPause
-
-
-
-
-
-
-
-
-
-
- onSaveInstanceState
-
-
-
-
-
-
-
-
-
- onRetainNonConfigurationInstance
-
-
-
-
-
-
-
-
-
- onDestroyView
-
-
-
-
-
-
-
-
-
- onDestroy
-
-
-
-
-
-
-
-
-
- onDetach
-
-
-
-
-
-
-
-
-
- onRequestPermissionsResult
-
-
-
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
- true
- true
- true
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
-
-
-
-
-
-
-
-
- xmlns:.*
-
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
- ^$
-
-
-
-
-
-
-
-
- style
- ^$
-
-
-
-
-
-
-
-
- .*
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:layout_width
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_height
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_.*
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*:width
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*:height
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Droidcon-Boston/.idea/codeStyles/Project.xml b/Droidcon-Boston/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..d306041
--- /dev/null
+++ b/Droidcon-Boston/.idea/codeStyles/Project.xml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+ ^$
+
+
+
+
+
+
+
+
+ style
+ ^$
+
+
+
+
+
+
+
+
+ .*
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/.idea/codeStyles/codeStyleConfig.xml b/Droidcon-Boston/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..12d7f3e
--- /dev/null
+++ b/Droidcon-Boston/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/build.gradle b/Droidcon-Boston/app/build.gradle
index 7f7117e..6e1844d 100644
--- a/Droidcon-Boston/app/build.gradle
+++ b/Droidcon-Boston/app/build.gradle
@@ -1,22 +1,42 @@
+import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
+
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
apply plugin: 'io.fabric'
+apply plugin: 'com.novoda.static-analysis'
+apply plugin: "io.gitlab.arturbosch.detekt"
+apply plugin: "org.jlleitschuh.gradle.ktlint"
android {
- compileSdkVersion 27
+ compileSdkVersion 28
defaultConfig {
applicationId "com.mentalmachines.droidcon_boston"
- minSdkVersion 16
- targetSdkVersion 27
- versionCode 17
- versionName "2.0.9"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ minSdkVersion 21
+ targetSdkVersion 28
+ versionCode 21
+ versionName "3.0.3"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ multiDexEnabled true
vectorDrawables {
useSupportLibrary = true
}
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ includeCompileClasspath true
+ }
+ }
+
+ buildConfigField "int", "EVENT_YEAR", "2019"
+ buildConfigField "int", "EVENT_MONTH", "04"
+ buildConfigField "int", "EVENT_DAY_ONE", "8"
+ buildConfigField "int", "EVENT_DAY_TWO", "9"
+ buildConfigField "String", "EVENT_DAY_ONE_STRING", "\"04/08/2019\""
+ buildConfigField "String", "EVENT_DAY_TWO_STRING", "\"04/09/2019\""
}
signingConfigs {
@@ -50,45 +70,111 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
+ androidExtensions {
+ experimental = true
+ }
+}
+
+staticAnalysis {
+ penalty {
+ maxErrors = 150 // current threshold value
+ maxWarnings = 70 // current threshold value
+ }
+
+ lintOptions {
+ lintConfig rootProject.file('team-props/lint-config.xml')
+ checkReleaseBuilds false
+ warningsAsErrors true
+ }
+
+ detekt {
+ config = rootProject.files('team-props/detekt-config.yml')
+ filters = '.*test.*,.*/resources/.*,.*/tmp/.*'
+ }
+
+ ktlint {
+ android true
+ reporters = [ReporterType.CHECKSTYLE]
+
+ includeVariants { variant -> variant.name.contains('debug') }
+ }
}
dependencies {
- final support = "27.1.0"
- final playServices = "11.8.0"
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ //region Version
final junit = "4.12"
+ final support = '1.0.0'
+ final appCompat = '1.0.2'
+ final firebaseDatabase = '16.0.5'
+ final firebaseCore = '16.0.8'
+ final firebaseMessaging = '17.4.0'
+ final crashlytics = '2.9.1'
+ final gson = '2.8.2'
+ final flexibleAdapter = '5.0.1'
+ final flexibleAdapterUi = '1.0.0-b3'
+ final glide = '3.7.0'
+ final threeTen = '1.3.3'
+ final threeTenAbp = '1.0.5'
+ final recyclerRefreshLayout = '2.0.5'
+ final mockitoCore = '2.23.4'
+ final archCore = '2.0.0'
+ final room = "2.1.0-alpha04"
+ final timber = '4.7.1'
+ final lottieVersion = '3.0.0-beta2'
+ //endregion
+
+ //region Dependencies
+
+ //Room annotation processor
+ kapt "androidx.room:room-compiler:$room"
- implementation fileTree(dir: 'libs', include: ['*.jar'])
+ // Kotlin
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// TESTING
testImplementation "junit:junit:$junit"
+ testImplementation "org.threeten:threetenbp:$threeTen"
+ testImplementation "org.mockito:mockito-core:$mockitoCore"
+ testImplementation "androidx.arch.core:core-testing:$archCore"
// Support
- implementation "com.android.support:appcompat-v7:$support"
- implementation "com.android.support:support-v4:$support"
- implementation "com.android.support:cardview-v7:$support"
- implementation "com.android.support:design:$support"
- implementation "com.android.support:customtabs:$support"
+ implementation "androidx.appcompat:appcompat:$appCompat"
+ implementation "androidx.legacy:legacy-support-v4:$support"
+ implementation "androidx.cardview:cardview:$support"
+ implementation "com.google.android.material:material:$support"
+ implementation "androidx.browser:browser:$support"
+ implementation "androidx.lifecycle:lifecycle-extensions:$support"
+ implementation "androidx.core:core:$support"
+
+ //Room
+ implementation "androidx.room:room-runtime:$room"
// Firebase
- implementation "com.google.firebase:firebase-messaging:$playServices"
- implementation "com.google.firebase:firebase-database:$playServices"
- implementation "com.google.firebase:firebase-core:$playServices"
+ implementation "com.google.firebase:firebase-messaging:$firebaseMessaging"
+ implementation "com.google.firebase:firebase-database:$firebaseDatabase"
+ implementation "com.google.firebase:firebase-core:$firebaseCore"
+
+ implementation 'com.firebaseui:firebase-ui-auth:4.3.1'
+ implementation 'com.twitter.sdk.android:twitter-core:3.3.0'
// Crashlytics only for release builds
- releaseImplementation('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') {
+ releaseImplementation("com.crashlytics.sdk.android:crashlytics:$crashlytics@aar") {
transitive = true
}
// Misc
- implementation 'com.google.code.gson:gson:2.8.2'
- implementation 'eu.davidea:flexible-adapter:5.0.1'
- implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b3'
- implementation 'com.github.bumptech.glide:glide:3.7.0'
- implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5'
- implementation 'com.dinuscxj:recyclerrefreshlayout:2.0.5'
-
- // Kotlin
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "com.google.code.gson:gson:$gson"
+ implementation "eu.davidea:flexible-adapter:$flexibleAdapter"
+ implementation "eu.davidea:flexible-adapter-ui:$flexibleAdapterUi"
+ implementation "com.github.bumptech.glide:glide:$glide"
+ implementation "com.jakewharton.threetenabp:threetenabp:$threeTenAbp"
+ implementation "com.dinuscxj:recyclerrefreshlayout:$recyclerRefreshLayout"
+ implementation "com.jakewharton.timber:timber:$timber"
+ implementation "com.airbnb.android:lottie:$lottieVersion"
+ //endregion
}
apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
diff --git a/Droidcon-Boston/app/google-services.json b/Droidcon-Boston/app/google-services.json
index 4b01613..5536e7e 100644
--- a/Droidcon-Boston/app/google-services.json
+++ b/Droidcon-Boston/app/google-services.json
@@ -1,7 +1,7 @@
{
"project_info": {
"project_number": "1084321061238",
- "firebase_url": "https://droidcon-bos.firebaseio.com",
+ "firebase_url": "https://droidcon-bos-2019.firebaseio.com",
"project_id": "droidcon-bos",
"storage_bucket": "droidcon-bos.appspot.com"
},
diff --git a/Droidcon-Boston/app/sampledata/sample_eb_result.json b/Droidcon-Boston/app/sampledata/sample_eb_result.json
new file mode 100644
index 0000000..674f469
--- /dev/null
+++ b/Droidcon-Boston/app/sampledata/sample_eb_result.json
@@ -0,0 +1,63 @@
+{
+ "profile": {
+ "first_name": "Christopher",
+ "last_name": "Corrado",
+ "company": "Johnson & Johnson",
+ "name": "Christopher Corrado",
+ "email": "ccorrads@gmail.com",
+ "job_title": "Senior Software Engineer"
+ },
+ "answers": [
+ {
+ "answer": "Developer",
+ "question": "Which is your main function in your current team?",
+ "type": "multiple_choice",
+ "question_id": "20940485"
+ },
+ {
+ "answer": "@ccorrads",
+ "question": "What's your Twitter handle?",
+ "type": "text",
+ "question_id": "20940481"
+ },
+ {
+ "answer": "Yes",
+ "question": "Would you like to receive the Droidcon Boston newsletter?",
+ "type": "multiple_choice",
+ "question_id": "20940482"
+ },
+ {
+ "answer": "N/A",
+ "question": "Do you have any food restrictions?",
+ "type": "text",
+ "question_id": "20940483"
+ },
+ {
+ "answer": "Advanced topics | Technology landscape topics | Networking with peers | Recruiting | An excuse to get out of the office",
+ "question": "Which is your main goal for Droidcon Boston 2019?",
+ "type": "multiple_choice",
+ "question_id": "20940484"
+ },
+ {
+ "answer": "M",
+ "question": "Unisex t-shirt size",
+ "type": "multiple_choice",
+ "question_id": "20940486"
+ },
+ {
+ "answer": "Meetup",
+ "question": "How did you hear about Droidcon?",
+ "type": "multiple_choice",
+ "question_id": "20940487"
+ },
+ {
+ "question": "Where?",
+ "type": "text",
+ "question_id": "20940488"
+ }
+ ],
+ "checked_in": false,
+ "cancelled": false,
+ "status": "Attending",
+ "event_id": "51797973132"
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/AndroidManifest.xml b/Droidcon-Boston/app/src/main/AndroidManifest.xml
index a10c596..15cb15f 100644
--- a/Droidcon-Boston/app/src/main/AndroidManifest.xml
+++ b/Droidcon-Boston/app/src/main/AndroidManifest.xml
@@ -29,7 +29,9 @@
+ android:launchMode="singleTop"
+ android:screenOrientation="portrait"
+ android:taskAffinity="" />
{
+
+ /**
+ * Insert an object in the database.
+ *
+ * @param obj the object to be inserted.
+ */
+ @Insert
+ fun insert(obj: T)
+
+ /**
+ * Insert an array of objects in the database.
+ *
+ * @param obj the objects to be inserted.
+ */
+ @Insert
+ fun insert(vararg obj: T)
+
+ /**
+ * Insert list of objects in the database.
+ *
+ * @param objs the objects to be inserted.
+ */
+ @Insert
+ fun insertAll(objs: List)
+
+ /**
+ * Delete an object from the database
+ *
+ * @param obj the object to be deleted
+ */
+ @Delete
+ fun delete(obj: T)
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/DataSource.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/DataSource.kt
new file mode 100644
index 0000000..eabf3a5
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/DataSource.kt
@@ -0,0 +1,7 @@
+package com.mentalmachines.droidcon_boston.data
+
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+interface DataSource {
+ fun getTweets(): List
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/EventbriteUser.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/EventbriteUser.kt
new file mode 100644
index 0000000..bf95706
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/EventbriteUser.kt
@@ -0,0 +1,33 @@
+package com.mentalmachines.droidcon_boston.data
+
+import com.google.gson.annotations.SerializedName
+
+data class EventbriteUser(
+ var id: String?,
+ var profile: EventbriteProfile,
+ var answers: List
+)
+
+data class EventbriteProfile(
+ var first_name: String,
+ var last_name: String,
+ var company: String,
+ var name: String,
+ var email: String,
+ var job_title: String
+)
+
+data class EventbriteQuestion(
+ var answer: String?,
+ var question: String,
+ //20940481 is the question_id for the twitter handle in the response.
+ var question_id: String,
+ var type: QuestionType?
+)
+
+enum class QuestionType {
+ @SerializedName("mutliple_choice")
+ MultipleChoice,
+ @SerializedName("text")
+ Text
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabase.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabase.kt
index aa62750..17f4d48 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabase.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabase.kt
@@ -11,37 +11,48 @@ import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.format.DateTimeFormatter
+import java.util.*
+import kotlin.collections.HashMap
+const val TIME_BETWEEN_SESSIONS: Long = 15
open class FirebaseDatabase {
data class ScheduleEvent(
- private val SESSION_REMINDER_MINUTES_BEFORE: Long = 10,
-
- var primarySpeakerName: String = "",
- var startTime: String = "",
- var name: String = "",
-
- var speakerNames: HashMap = HashMap(0),
- var speakerNameToPhotoUrl: HashMap = HashMap(0),
- var speakerNameToOrg: HashMap = HashMap(0),
- var roomNames: HashMap = HashMap(0),
- var speakerIds: HashMap = HashMap(0),
- var roomIds: HashMap = HashMap(0),
- var description: String = "",
- var photo: HashMap = HashMap(0),
- var endTime: String = "",
- var trackSortOrder: Int = 0) {
-
- val conferenceTZ = ZoneId.of( "America/New_York" )
+ private val SESSION_REMINDER_MINUTES_BEFORE: Long = 10,
+
+ var primarySpeakerName: String = "",
+ var startTime: String = "",
+ var name: String = "",
+
+ var speakerNames: HashMap = HashMap(0),
+ var speakerNameToPhotoUrl: HashMap = HashMap(0),
+ var speakerNameToOrg: HashMap = HashMap(0),
+ var roomNames: HashMap = HashMap(0),
+ var speakerIds: HashMap = HashMap(0),
+ var roomIds: HashMap = HashMap(0),
+ var description: String = "",
+ var photo: HashMap = HashMap(0),
+ var endTime: String = "",
+ var trackSortOrder: Int = 0
+ ) {
+
+ private val conferenceTZ: ZoneId = ZoneId.of("America/New_York")
fun getLocalStartTime(): LocalDateTime {
- return ZonedDateTime.parse(startTime).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime()
+ return ZonedDateTime.parse(startTime).withZoneSameInstant(ZoneId.systemDefault())
+ .toLocalDateTime()
}
fun scheduleNotification(context: Context, eventId: String, sessionDetail: ScheduleRow) {
- NotificationUtils(context).scheduleNotificationAlarm(getLocalStartTime().minusMinutes(SESSION_REMINDER_MINUTES_BEFORE),
- eventId, context.getString(R.string.str_session_start_soon, name), description.getHtmlFormattedSpanned().toString(),
- ServiceLocator.gson.toJson(sessionDetail, ScheduleRow::class.java))
+ NotificationUtils(context).scheduleNotificationAlarm(
+ getLocalStartTime().minusMinutes(
+ SESSION_REMINDER_MINUTES_BEFORE
+ ),
+ eventId,
+ context.getString(R.string.str_session_start_soon, name),
+ description.getHtmlFormattedSpanned().toString(),
+ ServiceLocator.gson.toJson(sessionDetail, ScheduleRow::class.java)
+ )
}
fun toScheduleRow(scheduleId: String): ScheduleRow {
@@ -68,7 +79,7 @@ open class FirebaseDatabase {
}
row.id = scheduleId
- row.room = roomNames.keys.first()
+ row.room = roomNames.keys.firstOrNull()
row.trackSortOrder = trackSortOrder
row.primarySpeakerName = primarySpeakerName
row.speakerNames = speakerNames.keys.toList()
@@ -77,17 +88,28 @@ open class FirebaseDatabase {
row.talkTitle = name
row.speakerNameToOrgName = speakerNameToOrg
row.photoUrlMap = speakerNameToPhotoUrl
+
+ if (startDateTime != null && endDateTime != null) {
+ val now = ZonedDateTime.now()
+ if (now.isAfter(startDateTime.minusMinutes(TIME_BETWEEN_SESSIONS)) &&
+ now.isBefore(endDateTime)
+ ) {
+ row.isCurrentSession = true
+ }
+ }
+
return row
}
}
data class EventSpeaker(
- val pictureUrl: String = "",
- val socialProfiles: HashMap? = HashMap(0),
- var bio: String = "",
- var title: String = "",
- var org: String = "",
- var name: String = "") {
+ val pictureUrl: String = "",
+ val socialProfiles: HashMap? = HashMap(0),
+ var bio: String = "",
+ var title: String = "",
+ var org: String = "",
+ var name: String = ""
+ ) {
fun toScheduleDetail(listRow: ScheduleRow): ScheduleDetail {
val detail = ScheduleDetail(listRow)
@@ -104,23 +126,33 @@ open class FirebaseDatabase {
}
data class VolunteerEvent(
- val twitter: String = "",
- val pictureUrl: String = "",
- var position: String = "",
- var firstName: String = "",
- var lastName: String = "")
+ val twitter: String = "",
+ val pictureUrl: String = "",
+ var position: String = "",
+ var firstName: String = "",
+ var lastName: String = ""
+ )
class FaqEvent {
data class Answer(
- var answer: String = "",
- var photoLink: String = "",
- var mapLink: String = "",
- var otherLink: String = ""
+ var answer: String = "",
+ var photoLink: String = "",
+ var mapLink: String = "",
+ var otherLink: String = ""
)
var answers: List = emptyList()
var question: String = ""
}
+
+ data class User(
+ val id: String? = "",
+ val username: String? = "",
+ val pictureUrl: String? = "",
+ val displayName: String? = "",
+ val twitter: String? = "",
+ val savedSessionIds: Map = mutableMapOf()
+ )
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/LocalDataSource.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/LocalDataSource.kt
new file mode 100644
index 0000000..57763b7
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/LocalDataSource.kt
@@ -0,0 +1,52 @@
+package com.mentalmachines.droidcon_boston.data
+
+import android.content.Context
+import com.mentalmachines.droidcon_boston.modal.Media
+import com.mentalmachines.droidcon_boston.modal.QuotedTweet
+import com.mentalmachines.droidcon_boston.modal.Tweet
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+class LocalDataSource private constructor(private val appDatabase: AppDatabase) : DataSource {
+
+ companion object {
+ private var dataSource: LocalDataSource? = null
+ fun getInstance(context: Context): LocalDataSource {
+ return dataSource ?: LocalDataSource(AppDatabase.getInstance(context)).also {
+ dataSource = it
+ }
+ }
+ }
+
+ override fun getTweets(): List {
+ return appDatabase.tweetDao().getTweets()
+ }
+
+ fun updateTweets(tweets: List) {
+ segregateEntity(tweets)
+ }
+
+ private fun segregateEntity(tweetsWithMedia: List) {
+ val tweets: MutableList = arrayListOf()
+ val quotedTweet: MutableList = arrayListOf()
+ val media: MutableList = arrayListOf()
+
+ tweetsWithMedia.forEach { tweetWithMedia ->
+ tweets.add(tweetWithMedia.tweet)
+ tweetWithMedia.media?.let { media.addAll(it) }
+ tweetWithMedia.quotedMedia?.let { media.addAll(it) }
+ tweetWithMedia.tweet.quotedTweet?.let { quotedTweet.add(it) }
+ }
+
+ if (tweets.isNotEmpty()) {
+ appDatabase.tweetDao().updateTweets(tweets)
+ }
+
+ if (quotedTweet.isNotEmpty()) {
+ appDatabase.quotedTweetDao().updateTweets(quotedTweet)
+ }
+
+ if (media.isNotEmpty()) {
+ appDatabase.mediaDao().insertAll(media)
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/MediaDao.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/MediaDao.kt
new file mode 100644
index 0000000..681157a
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/MediaDao.kt
@@ -0,0 +1,7 @@
+package com.mentalmachines.droidcon_boston.data
+
+import androidx.room.Dao
+import com.mentalmachines.droidcon_boston.modal.Media
+
+@Dao
+abstract class MediaDao : BaseDao
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/QuotedTweetDao.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/QuotedTweetDao.kt
new file mode 100644
index 0000000..44a5272
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/QuotedTweetDao.kt
@@ -0,0 +1,19 @@
+package com.mentalmachines.droidcon_boston.data
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import com.mentalmachines.droidcon_boston.modal.QuotedTweet
+
+@Dao
+abstract class QuotedTweetDao : BaseDao {
+
+ @Transaction
+ open fun updateTweets(tweets: List) {
+ deleteAll()
+ insertAll(tweets)
+ }
+
+ @Query("DELETE FROM QuotedTweet")
+ abstract fun deleteAll()
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RemoteDataSource.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RemoteDataSource.kt
new file mode 100644
index 0000000..f953e53
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RemoteDataSource.kt
@@ -0,0 +1,116 @@
+package com.mentalmachines.droidcon_boston.data
+
+import androidx.annotation.LayoutRes
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.modal.Media
+import com.mentalmachines.droidcon_boston.modal.QuotedTweet
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+import com.mentalmachines.droidcon_boston.utils.toDate
+import com.twitter.sdk.android.core.TwitterCore
+import com.twitter.sdk.android.core.models.Tweet
+import com.mentalmachines.droidcon_boston.modal.Tweet as ViewTweet
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class RemoteDataSource : DataSource {
+
+ companion object {
+
+ private const val TWEET_COUNT = 100
+
+ private lateinit var dataSource: RemoteDataSource
+
+ fun getInstance(): RemoteDataSource {
+ if (!::dataSource.isInitialized) {
+ dataSource = RemoteDataSource()
+ }
+ return dataSource
+ }
+ }
+
+ override fun getTweets(): List {
+ val response = TwitterCore
+ .getInstance()
+ .guestApiClient
+ .searchService
+ .tweets(
+ "%23DroidconBos",
+ null,
+ null,
+ null,
+ "latest",
+ TWEET_COUNT,
+ null,
+ 0,
+ 0,
+ true
+ ).execute()
+ return mapTweets(response.body()?.tweets.orEmpty())
+ }
+
+ private fun mapTweets(tweets: List): List {
+ val simpleDateFormat = SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US)
+ return tweets.distinctBy {
+ when {
+ it.retweetedStatus != null -> it.retweetedStatus.id
+ else -> it.id
+ }
+ }.map {
+ when {
+ it.retweetedStatus != null -> {
+ val type =
+ if (it.retweetedStatus.quotedStatus != null) R.layout.quoted_tweet_item else
+ R.layout.tweet_item_layout
+
+ mapTweetToTweetWithMedia(type, it.retweetedStatus, simpleDateFormat)
+ }
+ it.quotedStatus != null -> {
+ mapTweetToTweetWithMedia(R.layout.quoted_tweet_item, it, simpleDateFormat)
+ }
+ else -> {
+ mapTweetToTweetWithMedia(R.layout.tweet_item_layout, it, simpleDateFormat)
+ }
+ }
+ }
+ }
+
+ private fun mapTweetToTweetWithMedia(
+ @LayoutRes type: Int, tweet: Tweet,
+ simpleDateFormat: SimpleDateFormat
+ ): TweetWithMedia {
+ val quotedTweet = tweet.quotedStatus?.run {
+ QuotedTweet(
+ id,
+ user.screenName,
+ user.name,
+ user.profileImageUrlHttps,
+ text
+ )
+ }
+ return TweetWithMedia().apply {
+ this.tweet = ViewTweet(
+ tweet.id,
+ tweet.createdAt.toDate(simpleDateFormat),
+ type,
+ tweet.user.screenName,
+ tweet.user.name,
+ tweet.user.profileImageUrlHttps,
+ tweet.text,
+ quotedTweet
+ )
+
+ media = tweet.extendedEntities?.media?.map { media ->
+ Media(
+ media.id, tweet.id, media.type, media.mediaUrlHttps, media.url, tweet.id
+ )
+ }
+
+ quotedMedia = tweet.quotedStatus?.extendedEntities?.media?.map { media ->
+ Media(
+ media.id, tweet.id, media.type, media.mediaUrlHttps,
+ media.url, quotedTweetId = tweet.quotedStatus.id
+ )
+ }
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Repository.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Repository.kt
new file mode 100644
index 0000000..22eac89
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Repository.kt
@@ -0,0 +1,36 @@
+package com.mentalmachines.droidcon_boston.data
+
+import android.content.Context
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+class Repository private constructor(
+ private val remoteDataSource: RemoteDataSource,
+ private val localDataSource: LocalDataSource
+) {
+
+ companion object {
+
+ private lateinit var repository: Repository
+
+ fun getInstance(context: Context): Repository {
+ if (!::repository.isInitialized) {
+ repository = Repository(
+ RemoteDataSource.getInstance(), LocalDataSource.getInstance(context)
+ )
+ }
+ return repository
+ }
+ }
+
+ fun getTweets(): List {
+ return remoteDataSource.getTweets()
+ }
+
+ fun getTweetsFromDB(): List {
+ return localDataSource.getTweets()
+ }
+
+ fun updateDBTweets(tweets: List) {
+ localDataSource.updateTweets(tweets)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RoomConverters.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RoomConverters.kt
new file mode 100644
index 0000000..3c7c3c5
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/RoomConverters.kt
@@ -0,0 +1,17 @@
+package com.mentalmachines.droidcon_boston.data
+
+import androidx.room.TypeConverter
+import java.util.Date
+
+class RoomConverters {
+
+ @TypeConverter
+ fun fromTimestamp(value: Long?): Date? {
+ return value?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun dateToTimestamp(date: Date?): Long? {
+ return date?.time
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Schedule.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Schedule.kt
index 08df8eb..cca228b 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Schedule.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/Schedule.kt
@@ -1,48 +1,59 @@
package com.mentalmachines.droidcon_boston.data
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+import java.util.*
+
/*
* View models for schedule and schedule detail items.
*/
class Schedule {
+ @Parcelize
data class ScheduleRow(
- var primarySpeakerName: String = "",
- var id: String = "",
- var startTime: String = "",
- var talkTitle: String = "",
- var speakerCount: Int = 0,
- var talkDescription: String = "",
- var speakerNames: List = emptyList(),
- var speakerNameToOrgName: HashMap = HashMap(0),
- var utcStartTimeString: String = "",
- var endTime: String = "",
- var room: String = "",
- var date: String = "",
- var trackSortOrder: Int = 0,
- var photoUrlMap: HashMap = HashMap(0),
- var isOver: Boolean = false) {
+ var primarySpeakerName: String = "",
+ var id: String = "",
+ var startTime: String = "",
+ var talkTitle: String = "",
+ var speakerCount: Int = 0,
+ var talkDescription: String = "",
+ var speakerNames: List = emptyList(),
+ var speakerNameToOrgName: HashMap = HashMap(0),
+ var utcStartTimeString: String = "",
+ var endTime: String = "",
+ var room: String? = null,
+ var date: String = "",
+ var trackSortOrder: Int = 0,
+ var photoUrlMap: HashMap = HashMap(0),
+ var isOver: Boolean = false,
+ var isCurrentSession: Boolean = false
+ ) : Parcelable {
fun hasSpeaker(): Boolean = speakerNames.isNotEmpty()
fun hasMultipleSpeakers(): Boolean = speakerNames.size > 1
fun getSpeakerString(): String? = speakerNames.joinToString(", ")
- }
- class ScheduleDetail(val listRow: ScheduleRow) {
+ fun containsKeyword(keyword: String): Boolean {
+ return this.talkTitle.contains(keyword, ignoreCase = true)
+ || this.talkDescription.contains(keyword, ignoreCase = true)
+ || this.speakerNames.any { it.contains(keyword, ignoreCase = true) }
+ }
+ }
- var speakerBio: String = ""
- var twitter: String = ""
- var linkedIn: String = ""
+ data class ScheduleDetail(
+ val listRow: ScheduleRow,
+ var speakerBio: String = "",
+ var twitter: String = "",
+ var linkedIn: String = "",
var facebook: String = ""
-
+ ) {
val id: String get() = listRow.id
}
companion object {
var SCHEDULE_ITEM_ROW = "schedule_item_row"
- var MONDAY = "03/26/2018"
- var TUESDAY = "03/27/2018"
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/TweetDao.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/TweetDao.kt
new file mode 100644
index 0000000..24016aa
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/TweetDao.kt
@@ -0,0 +1,24 @@
+package com.mentalmachines.droidcon_boston.data
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import com.mentalmachines.droidcon_boston.modal.Tweet
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+@Dao
+abstract class TweetDao : BaseDao {
+
+ @Transaction
+ open fun updateTweets(tweets: List) {
+ deleteAll()
+ insertAll(tweets)
+ }
+
+ @Transaction
+ @Query("SELECT * FROM Tweet ORDER BY createdAt DESC")
+ abstract fun getTweets(): List
+
+ @Query("DELETE FROM Tweet")
+ abstract fun deleteAll()
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepo.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepo.kt
index 66ebe5f..92603f8 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepo.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepo.kt
@@ -8,24 +8,26 @@ import com.mentalmachines.droidcon_boston.utils.SingletonHolder
class UserAgendaRepo private constructor(context: Context) {
private val prefsKey = "UserAgenda"
private val sessionIdsKey = "savedSessionsIds"
- private val sharedPrefs : SharedPreferences = context.getSharedPreferences(prefsKey, MODE_PRIVATE)
- private val savedSessionIds = HashSet()
+ private val sharedPrefs: SharedPreferences =
+ context.getSharedPreferences(prefsKey, MODE_PRIVATE)
+ val savedSessionIds = HashMap()
init {
- savedSessionIds.addAll(sharedPrefs.getStringSet(sessionIdsKey, HashSet()))
+ savedSessionIds += sharedPrefs.getStringSet(sessionIdsKey, HashSet()).orEmpty()
+ .map { it to it }.toMap()
}
- fun isSessionBookmarked(sessionId : String) : Boolean {
+ fun isSessionBookmarked(sessionId: String): Boolean {
return savedSessionIds.contains(sessionId)
}
- fun bookmarkSession(sessionId : String, flag : Boolean) {
+ fun bookmarkSession(sessionId: String, flag: Boolean) {
if (flag) {
- savedSessionIds.add(sessionId)
+ savedSessionIds.put(sessionId, sessionId)
} else {
savedSessionIds.remove(sessionId)
}
- sharedPrefs.edit().putStringSet(sessionIdsKey, savedSessionIds).apply()
+ sharedPrefs.edit().putStringSet(sessionIdsKey, savedSessionIds.values.toSet()).apply()
}
companion object : SingletonHolder(::UserAgendaRepo)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TaskScheduler.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TaskScheduler.kt
new file mode 100644
index 0000000..22640f9
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TaskScheduler.kt
@@ -0,0 +1,26 @@
+package com.mentalmachines.droidcon_boston.domain
+
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+/**
+ * Didn't wanted to add dependency for thread management like Rx or coroutine only for twitter
+ * feed feature hence this implementation of thread pool executor.
+ * Stolen from google IO app and removed unnecessary stuff ;)
+ * Check more about it here
+ * https://github.com/google/iosched/blob/master/shared/src/main/java/com/google/samples/apps/iosched/shared/domain/internal/TaskScheduler.kt
+ */
+private const val NUMBER_OF_THREADS = 4
+
+interface Scheduler {
+ fun execute(task: () -> Unit)
+}
+
+object AsyncScheduler : Scheduler {
+
+ private val executorService: ExecutorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS)
+
+ override fun execute(task: () -> Unit) {
+ executorService.execute(task)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TwitterUseCase.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TwitterUseCase.kt
new file mode 100644
index 0000000..f884866
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/TwitterUseCase.kt
@@ -0,0 +1,27 @@
+package com.mentalmachines.droidcon_boston.domain
+
+import com.mentalmachines.droidcon_boston.data.Repository
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+import java.lang.Exception
+
+class TwitterUseCase(private val repository: Repository) : UseCase>() {
+
+ override fun execute(parameters: Unit): List {
+ val tweets = try {
+ repository.getTweets().run {
+ if (isEmpty()) {
+ repository.getTweetsFromDB()
+ } else {
+ taskScheduler.execute { repository.updateDBTweets(this) }
+ this
+ }
+ }
+ } catch (e: Exception) {
+ repository.getTweetsFromDB()
+ }
+ if (tweets.isEmpty()) {
+ throw Exception("Empty list")
+ }
+ return tweets
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/UseCase.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/UseCase.kt
new file mode 100644
index 0000000..7f25477
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/domain/UseCase.kt
@@ -0,0 +1,58 @@
+package com.mentalmachines.droidcon_boston.domain
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.mentalmachines.droidcon_boston.modal.Result
+import timber.log.Timber
+
+/**
+ * Executes business logic asynchronously using a [AsyncScheduler].
+ */
+abstract class UseCase {
+
+ val taskScheduler = AsyncScheduler
+
+ /** Executes the use case asynchronously and places the [Result] in a MutableLiveData
+ *
+ * @param parameters the input parameters to run the use case with
+ * @param result the MutableLiveData where the result is posted to
+ *
+ */
+ operator fun invoke(parameters: P, result: MutableLiveData>): LiveData> {
+ try {
+ result.postValue(Result.Loading)
+ taskScheduler.execute {
+ try {
+ execute(parameters).let { useCaseResult ->
+ result.postValue(Result.Data(useCaseResult))
+ }
+ } catch (e: Exception) {
+ Timber.e(e)
+ result.postValue(Result.Error(e.message ?: "Error"))
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e(e)
+ result.postValue(Result.Error(e.message ?: "Error"))
+ }
+ return result
+ }
+
+ /** Executes the use case asynchronously and returns a [Result] in a new LiveData object.
+ *
+ * @return an observable [LiveData] with a [Result].
+ *
+ * @param parameters the input parameters to run the use case with
+ */
+ operator fun invoke(parameters: P): LiveData> {
+ val liveCallback: MutableLiveData> = MutableLiveData()
+ this(parameters, liveCallback)
+ return liveCallback
+ }
+
+ /**
+ * Override this to set the code to be executed.
+ */
+ @Throws(RuntimeException::class)
+ protected abstract fun execute(parameters: P): R
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/AuthController.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/AuthController.kt
new file mode 100644
index 0000000..2b10927
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/AuthController.kt
@@ -0,0 +1,107 @@
+package com.mentalmachines.droidcon_boston.firebase
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import com.firebase.ui.auth.AuthUI
+import com.firebase.ui.auth.IdpResponse
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.FirebaseUser
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.ValueEventListener
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
+import timber.log.Timber
+
+object AuthController {
+
+ val isLoggedIn: Boolean
+ get() = (FirebaseAuth.getInstance().currentUser != null)
+
+ val userId: String?
+ get() = FirebaseAuth.getInstance().currentUser?.uid
+
+ fun login(activity: AppCompatActivity, resultCode: Int, @DrawableRes loginScreenAppIcon: Int) {
+ activity.startActivityForResult(
+ getAuthIntent(loginScreenAppIcon), resultCode
+ )
+ }
+
+ fun login(fragment: Fragment, resultCode: Int, @DrawableRes loginScreenAppIcon: Int) {
+ fragment.startActivityForResult(
+ getAuthIntent(loginScreenAppIcon), resultCode
+ )
+ }
+
+ private fun getAuthIntent(
+ loginScreenAppIcon: Int
+ ): Intent {
+ val providers = arrayListOf(
+ AuthUI.IdpConfig.GoogleBuilder().build()
+ )
+ return AuthUI.getInstance()
+ .createSignInIntentBuilder()
+ .setAvailableProviders(providers)
+ .setLogo(loginScreenAppIcon)
+ .build()
+ }
+
+ /***
+ * Returns an error message if there was a login error
+ * or null if successful
+ */
+ fun handleLoginResult(context: Context, resultCode: Int, data: Intent?): String? {
+ val response = IdpResponse.fromResultIntent(data)
+
+ return if (resultCode == Activity.RESULT_OK) {
+ FirebaseAuth.getInstance().currentUser?.let { user ->
+ FirebaseHelper.instance.userDatabase.addListenerForSingleValueEvent(object :
+ ValueEventListener {
+ override fun onCancelled(error: DatabaseError) {
+ Timber.e(error.toException())
+ }
+
+ override fun onDataChange(nodes: DataSnapshot) {
+ // only add user node if it doesn't exist so we don't overwrite favorites
+ if (!nodes.hasChild(user.uid)) {
+ FirebaseHelper.instance.userDatabase.child(user.uid)
+ .setValue(
+ createLocalUser(
+ UserAgendaRepo.getInstance(context).savedSessionIds
+ .values.toSet(),
+ user
+ )
+ )
+ }
+ }
+ })
+ }
+ null
+ } else {
+ response?.error?.message
+ }
+ }
+
+ fun logout(context: Context, completeCallback: () -> Unit) {
+ AuthUI.getInstance().signOut(context).addOnCompleteListener{
+ completeCallback()
+ }
+ }
+
+ @VisibleForTesting
+ fun createLocalUser(sessionIds: Set, user: FirebaseUser): FirebaseDatabase.User {
+ return FirebaseDatabase.User(
+ user.uid,
+ user.email,
+ user.photoUrl?.toString(),
+ user.displayName,
+ "",
+ sessionIds.map { it to it } .toMap()
+ )
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/DbFirebaseMessagingService.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/DbFirebaseMessagingService.kt
index 4c90685..4896f09 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/DbFirebaseMessagingService.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/DbFirebaseMessagingService.kt
@@ -1,15 +1,13 @@
package com.mentalmachines.droidcon_boston.firebase
-import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.utils.NotificationUtils
+import timber.log.Timber
class DbFirebaseMessagingService : FirebaseMessagingService() {
- private val TAG = javaClass.simpleName
-
override fun onMessageReceived(remoteMessage: RemoteMessage?) {
if (remoteMessage != null) {
val payloadMap = remoteMessage.data
@@ -17,9 +15,9 @@ class DbFirebaseMessagingService : FirebaseMessagingService() {
// Check if message contains a data payload.
if (payloadMap.isNotEmpty()) {
- Log.d(TAG, "Payload: ")
+ Timber.d("Payload: ")
for (key in payloadMap.keys) {
- Log.d(TAG, "Key: " + key + ", Value: " + payloadMap[key])
+ Timber.d("Key: $key, Value: ${payloadMap[key]}")
}
NotificationUtils(applicationContext).scheduleMySessionNotifications()
}
@@ -27,12 +25,20 @@ class DbFirebaseMessagingService : FirebaseMessagingService() {
// Check if message contains a notification payload.
if (remoteMessage.notification != null) {
bodyStr = remoteMessage.notification!!.body
- Log.d(TAG, "Body: " + bodyStr!!)
+ Timber.d("Body: $bodyStr")
}
// Show the notification here
val notificationUtils = NotificationUtils(this)
- notificationUtils.sendAndroidChannelNotification(getString(R.string.conference_name), bodyStr!!, 101)
+ notificationUtils.sendAndroidChannelNotification(
+ getString(R.string.conference_name),
+ bodyStr!!,
+ NOTIFICATION_ID
+ )
}
}
+
+ companion object {
+ private const val NOTIFICATION_ID = 101
+ }
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelper.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelper.kt
index 4440cbd..1fd8e81 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelper.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelper.kt
@@ -6,27 +6,27 @@ import com.google.firebase.database.FirebaseDatabase
class FirebaseHelper private constructor() {
- private val database: FirebaseDatabase
+ private val database: FirebaseDatabase = FirebaseDatabase.getInstance()
private val mainDatabase: DatabaseReference
val eventDatabase: DatabaseReference
val speakerDatabase: DatabaseReference
val aboutDatabase: DatabaseReference
val faqDatabase: DatabaseReference
val cocDatabase: DatabaseReference
- val volunteerDatabase : DatabaseReference
+ val volunteerDatabase: DatabaseReference
+ val userDatabase: DatabaseReference
init {
- this.database = FirebaseDatabase.getInstance()
-
// Enable disk persistence, https://firebase.google.com/docs/database/android/offline-capabilities
- this.database.setPersistenceEnabled(true)
- this.mainDatabase = database.reference
- this.eventDatabase = mainDatabase.child("conferenceData").child("events")
- this.speakerDatabase = mainDatabase.child("conferenceData").child("speakers")
- this.volunteerDatabase = mainDatabase.child("volunteers")
- this.aboutDatabase = mainDatabase.child("about")
- this.faqDatabase = mainDatabase.child("faq")
- this.cocDatabase = mainDatabase.child("conductCode")
+ database.setPersistenceEnabled(true)
+ mainDatabase = database.reference
+ eventDatabase = mainDatabase.child("conferenceData").child("events")
+ speakerDatabase = mainDatabase.child("conferenceData").child("speakers")
+ volunteerDatabase = mainDatabase.child("volunteers")
+ aboutDatabase = mainDatabase.child("about")
+ faqDatabase = mainDatabase.child("faq")
+ cocDatabase = mainDatabase.child("conductCode")
+ userDatabase = mainDatabase.child("users")
}
private object Holder {
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Media.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Media.kt
new file mode 100644
index 0000000..6edef50
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Media.kt
@@ -0,0 +1,34 @@
+package com.mentalmachines.droidcon_boston.modal
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+
+@Entity(
+ primaryKeys = ["id1", "id2"],
+ foreignKeys = [ForeignKey(
+ entity = QuotedTweet::class, parentColumns = ["quoted_id"],
+ childColumns = ["quoted_tweet_id"], onDelete = ForeignKey.CASCADE
+ ),
+ ForeignKey(
+ entity = Tweet::class, parentColumns = ["id"], childColumns = ["tweet_id"],
+ onDelete = ForeignKey.CASCADE
+ )],
+ indices = [Index("tweet_id"), Index("quoted_tweet_id")]
+)
+data class Media(
+ val id1: Long,
+ val id2: Long,
+ val type: String,
+ val mediaUrlHttps: String,
+ val url: String,
+ @ColumnInfo(name = "tweet_id") val tweetId: Long? = null,
+ @ColumnInfo(name = "quoted_tweet_id") val quotedTweetId: Long? = null
+) {
+ companion object {
+ const val MEDIA_TYPE_PHOTO = "photo"
+ const val MEDIA_TYPE_GIF = "animated_gif"
+ const val MEDIA_TYPE_VIDEO = "video"
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/QuotedTweet.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/QuotedTweet.kt
new file mode 100644
index 0000000..f3ddba4
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/QuotedTweet.kt
@@ -0,0 +1,20 @@
+package com.mentalmachines.droidcon_boston.modal
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class QuotedTweet(
+ @PrimaryKey
+ @ColumnInfo(name = "quoted_id")
+ val id: Long,
+ @ColumnInfo(name = "quoted_name")
+ val name: String,
+ @ColumnInfo(name = "quoted_screenName")
+ val screenName: String,
+ @ColumnInfo(name = "quoted_profileImageUrl")
+ val profileImageUrl: String,
+ @ColumnInfo(name = "quoted_text")
+ val text: String
+)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Result.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Result.kt
new file mode 100644
index 0000000..51f498b
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Result.kt
@@ -0,0 +1,7 @@
+package com.mentalmachines.droidcon_boston.modal
+
+sealed class Result {
+ object Loading : Result()
+ class Data(val data: T) : Result()
+ class Error(val message: String) : Result()
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/SocialModal.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/SocialModal.kt
index eae4f2c..f8adc12 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/SocialModal.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/SocialModal.kt
@@ -1,3 +1,3 @@
package com.mentalmachines.droidcon_boston.modal
-class SocialModal(var image_resid: Int, var name: String?, var link: String?)
\ No newline at end of file
+class SocialModal(var image_resid: Int, var name: String?, var link: String?)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Tweet.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Tweet.kt
new file mode 100644
index 0000000..5f705b9
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/Tweet.kt
@@ -0,0 +1,18 @@
+package com.mentalmachines.droidcon_boston.modal
+
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.util.Date
+
+@Entity
+data class Tweet(
+ @PrimaryKey val id: Long,
+ val createdAt: Date,
+ val type: Int,
+ val name: String,
+ val screenName: String,
+ val profileImageUrl: String,
+ val text: String,
+ @Embedded val quotedTweet: QuotedTweet? = null
+)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/TweetWithMedia.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/TweetWithMedia.kt
new file mode 100644
index 0000000..31523d1
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/modal/TweetWithMedia.kt
@@ -0,0 +1,16 @@
+package com.mentalmachines.droidcon_boston.modal
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+class TweetWithMedia {
+
+ @Embedded
+ lateinit var tweet: Tweet
+
+ @Relation(parentColumn = "id", entityColumn = "tweet_id")
+ var media: List? = null
+
+ @Relation(parentColumn = "quoted_id", entityColumn = "quoted_tweet_id")
+ var quotedMedia: List? = null
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/BootReceiver.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/BootReceiver.kt
index 17d1fd8..c9e29b2 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/BootReceiver.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/BootReceiver.kt
@@ -1,8 +1,8 @@
package com.mentalmachines.droidcon_boston.receivers
-import android.content.Intent
import android.content.BroadcastReceiver
import android.content.Context
+import android.content.Intent
import com.mentalmachines.droidcon_boston.utils.NotificationUtils
@@ -13,4 +13,4 @@ class BootReceiver : BroadcastReceiver() {
NotificationUtils(context).scheduleMySessionNotifications()
}
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/NotificationPublisher.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/NotificationPublisher.kt
index b09e66c..445c199 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/NotificationPublisher.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/receivers/NotificationPublisher.kt
@@ -9,7 +9,8 @@ import android.content.Intent
class NotificationPublisher : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = intent.getParcelableExtra(NOTIFICATION)
val id = intent.getIntExtra(NOTIFICATION_ID, 0)
@@ -22,4 +23,4 @@ class NotificationPublisher : BroadcastReceiver() {
var NOTIFICATION = "notification"
var SESSION_ID = "session-id"
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/DividerItemDecoration.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/DividerItemDecoration.kt
index 89c6c8a..0512937 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/DividerItemDecoration.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/DividerItemDecoration.kt
@@ -4,11 +4,12 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
-import android.support.v4.view.ViewCompat
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.support.v7.widget.RecyclerView.State
import android.view.View
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.State
class DividerItemDecoration(context: Context, orientation: Int) : RecyclerView.ItemDecoration() {
@@ -30,11 +31,9 @@ class DividerItemDecoration(context: Context, orientation: Int) : RecyclerView.I
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
- val params = child
- .layoutParams as RecyclerView.LayoutParams
- @Suppress("DEPRECATION")
- val left = child.right + params.rightMargin +
- Math.round(ViewCompat.getTranslationX(child))
+ val params = child.layoutParams as RecyclerView.LayoutParams
+ @Suppress("DEPRECATION") val left =
+ child.right + params.rightMargin + Math.round(ViewCompat.getTranslationX(child))
val right = left + divider!!.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c!!)
@@ -48,26 +47,24 @@ class DividerItemDecoration(context: Context, orientation: Int) : RecyclerView.I
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
- val params = child
- .layoutParams as RecyclerView.LayoutParams
- @Suppress("DEPRECATION")
- val top = child.bottom + params.bottomMargin +
- Math.round(ViewCompat.getTranslationY(child))
+ val params = child.layoutParams as RecyclerView.LayoutParams
+ @Suppress("DEPRECATION") val top =
+ child.bottom + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child))
val bottom = top + divider!!.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c!!)
}
}
- override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: State?) {
+ override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
if (orientationValue == VERTICAL_LIST) {
- outRect?.set(0, 0, 0, divider!!.intrinsicHeight)
+ outRect.set(0, 0, 0, divider!!.intrinsicHeight)
} else {
- outRect?.set(0, 0, divider!!.intrinsicWidth, 0)
+ outRect.set(0, 0, divider!!.intrinsicWidth, 0)
}
}
- override fun onDraw(c: Canvas?, parent: RecyclerView?, state: State?) {
+ override fun onDraw(c: Canvas, parent: RecyclerView, state: State) {
if (orientationValue == VERTICAL_LIST) {
drawVertical(c, parent)
} else {
@@ -86,8 +83,8 @@ class DividerItemDecoration(context: Context, orientation: Int) : RecyclerView.I
private val ATTRS = intArrayOf(android.R.attr.listDivider)
- const val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL
+ const val HORIZONTAL_LIST = HORIZONTAL
- const val VERTICAL_LIST = LinearLayoutManager.VERTICAL
+ const val VERTICAL_LIST = VERTICAL
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/ExtensionFunctions.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/ExtensionFunctions.kt
index ac7bf59..555db2e 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/ExtensionFunctions.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/ExtensionFunctions.kt
@@ -2,32 +2,38 @@ package com.mentalmachines.droidcon_boston.utils
import android.content.Context
import android.net.Uri
-import android.support.customtabs.CustomTabsIntent
-import android.support.v4.content.ContextCompat
import android.text.Html
import android.text.Spanned
+import android.view.View
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.content.ContextCompat
import com.mentalmachines.droidcon_boston.R
-
+import java.text.SimpleDateFormat
+import java.util.Date
fun String?.isNullorEmpty(): Boolean {
return !(this != null && !this.isEmpty())
}
-
fun String.getHtmlFormattedSpanned(): Spanned {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT)
} else {
- @Suppress("DEPRECATION")
- Html.fromHtml(this)
+ @Suppress("DEPRECATION") Html.fromHtml(this)
}
}
-
fun Context.loadUriInCustomTab(uriString: String) {
val data = Uri.parse(uriString)
val customTabsIntent = CustomTabsIntent.Builder()
- .setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
- .build()
+ .setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)).build()
customTabsIntent.launchUrl(this, data)
-}
\ No newline at end of file
+}
+
+fun View?.visibleIf(condition: Boolean?) {
+ this?.visibility = if (condition == true) View.VISIBLE else View.GONE
+}
+
+fun String.toDate(simpleDateFormat: SimpleDateFormat): Date {
+ return simpleDateFormat.parse(this)
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/NotificationUtils.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/NotificationUtils.kt
index cd8777b..9bb097c 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/NotificationUtils.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/NotificationUtils.kt
@@ -14,9 +14,8 @@ import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Build.VERSION_CODES
-import android.support.v4.app.NotificationCompat
import android.text.TextUtils
-import android.util.Log
+import androidx.core.app.NotificationCompat
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
@@ -29,6 +28,7 @@ import com.mentalmachines.droidcon_boston.receivers.NotificationPublisher
import com.mentalmachines.droidcon_boston.views.MainActivity
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
+import timber.log.Timber
class NotificationUtils(context: Context) : ContextWrapper(context) {
@@ -38,11 +38,14 @@ class NotificationUtils(context: Context) : ContextWrapper(context) {
}
@TargetApi(VERSION_CODES.O)
- fun createChannels() {
+ private fun createChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// create android channel
- val androidChannel = NotificationChannel(ANDROID_CHANNEL_ID,
- ANDROID_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
+ val androidChannel = NotificationChannel(
+ ANDROID_CHANNEL_ID,
+ ANDROID_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
// Sets whether notifications posted to this channel should display notification lights
androidChannel.enableLights(true)
// Sets whether notification posted to this channel should vibrate.
@@ -61,18 +64,19 @@ class NotificationUtils(context: Context) : ContextWrapper(context) {
return getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
- private fun sendChannelNotification(title: String, body: String, notificationId: Int, channelId: String) {
+ private fun sendChannelNotification(
+ title: String,
+ body: String,
+ notificationId: Int,
+ channelId: String
+ ) {
val resultIntent = Intent(this, MainActivity::class.java)
- val pi = PendingIntent.getActivity(this, 0, resultIntent, PendingIntent
- .FLAG_UPDATE_CURRENT)
-
- val builder = NotificationCompat.Builder(applicationContext,
- channelId)
- .setContentTitle(title)
- .setContentText(body)
- .setTicker(getString(R.string.conference_name))
- .setSmallIcon(android.R.drawable.stat_notify_more)
- .setAutoCancel(true)
+ val pi = PendingIntent.getActivity(this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val builder =
+ NotificationCompat.Builder(applicationContext, channelId).setContentTitle(title)
+ .setContentText(body).setTicker(getString(R.string.conference_name))
+ .setSmallIcon(android.R.drawable.stat_notify_more).setAutoCancel(true)
// for notification click action, also required on Gingerbread and below
.setContentIntent(pi)
@@ -91,12 +95,19 @@ class NotificationUtils(context: Context) : ContextWrapper(context) {
override fun onDataChange(dataSnapshot: DataSnapshot) {
var hasBookmarkedEvents = false
for (roomSnapshot in dataSnapshot.children) {
- val eventId = roomSnapshot.key
- val scheduleEvent = roomSnapshot.getValue(FirebaseDatabase.ScheduleEvent::class.java)
+ val eventId = roomSnapshot.key ?: ""
+ val scheduleEvent =
+ roomSnapshot.getValue(FirebaseDatabase.ScheduleEvent::class.java)
scheduleEvent?.let {
- if (userRepo.isSessionBookmarked(eventId)
- && scheduleEvent.getLocalStartTime().isAfter(LocalDateTime.now())) {
- scheduleEvent.scheduleNotification(context, eventId, scheduleEvent.toScheduleRow(eventId))
+ if (userRepo.isSessionBookmarked(eventId) && scheduleEvent.getLocalStartTime().isAfter(
+ LocalDateTime.now()
+ )
+ ) {
+ scheduleEvent.scheduleNotification(
+ context,
+ eventId,
+ scheduleEvent.toScheduleRow(eventId)
+ )
hasBookmarkedEvents = true
}
}
@@ -107,16 +118,23 @@ class NotificationUtils(context: Context) : ContextWrapper(context) {
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.w("Notification", "scheduleQuery:onCancelled", databaseError.toException())
+ Timber.e(databaseError.toException())
firebaseHelper.eventDatabase.removeEventListener(this)
}
})
}
- fun scheduleNotificationAlarm(alarmTime: LocalDateTime, sessionId: String, title: String, body: String, sessionDetail: String) {
+ fun scheduleNotificationAlarm(
+ alarmTime: LocalDateTime,
+ sessionId: String,
+ title: String,
+ body: String,
+ sessionDetail: String
+ ) {
if (alarmTime.isAfter(LocalDateTime.now())) {
- val pendingIntent = getAgendaSessionNotificationPendingIntent(sessionId, title, body, sessionDetail)
+ val pendingIntent =
+ getAgendaSessionNotificationPendingIntent(sessionId, title, body, sessionDetail)
val utcInMillis = alarmTime.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC_WAKEUP, utcInMillis, pendingIntent)
@@ -132,37 +150,53 @@ class NotificationUtils(context: Context) : ContextWrapper(context) {
fun enableBootReceiver(context: Context, enabled: Boolean = true) {
val receiver = ComponentName(context, BootReceiver::class.java)
- val pm = context.getPackageManager()
-
- pm.setComponentEnabledSetting(receiver,
- if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- PackageManager.DONT_KILL_APP)
+ val pm = context.packageManager
+
+ pm.setComponentEnabledSetting(
+ receiver,
+ if (enabled) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ },
+ PackageManager.DONT_KILL_APP
+ )
}
- private fun getAgendaSessionNotificationPendingIntent(sessionId: String, title: String = "", body: String = "", sessionDetail: String = ""): PendingIntent {
- val builder = NotificationCompat.Builder(this, ANDROID_CHANNEL_ID)
- .setContentText(title)
- .setStyle(NotificationCompat.BigTextStyle()
- .bigText(body)
- .setBigContentTitle(title))
- .setSmallIcon(R.drawable.ic_notification_session_start)
- .setAutoCancel(true)
-
+ private fun getAgendaSessionNotificationPendingIntent(
+ sessionId: String,
+ title: String = "",
+ body: String = "",
+ sessionDetail: String = ""
+ ): PendingIntent {
+ val builder = NotificationCompat.Builder(this, ANDROID_CHANNEL_ID).setContentText(title)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(body).setBigContentTitle(title))
+ .setSmallIcon(R.drawable.ic_notification_session_start).setAutoCancel(true)
+
+ val notificationId = sessionId.hashCode()
if (!TextUtils.isEmpty(sessionDetail)) {
val sessionIntent = MainActivity.getSessionDetailIntent(this, sessionId, sessionDetail)
- val contentIntent = PendingIntent.getActivity(this, 0,
- sessionIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val contentIntent = PendingIntent.getActivity(
+ this,
+ notificationId,
+ sessionIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
builder.setContentIntent(contentIntent)
}
val notificationIntent = Intent(this, NotificationPublisher::class.java).apply {
- putExtra(NotificationPublisher.NOTIFICATION_ID, sessionId.hashCode())
+ putExtra(NotificationPublisher.NOTIFICATION_ID, notificationId)
putExtra(NotificationPublisher.SESSION_ID, sessionId)
putExtra(NotificationPublisher.NOTIFICATION, builder.build())
}
- val pendingIntent = PendingIntent.getBroadcast(this, sessionId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
- return pendingIntent
+ return PendingIntent.getBroadcast(
+ this,
+ notificationId,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
}
companion object {
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/RVItemClickListener.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/RVItemClickListener.kt
index ac698ff..0df8370 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/RVItemClickListener.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/RVItemClickListener.kt
@@ -1,18 +1,23 @@
package com.mentalmachines.droidcon_boston.utils
import android.content.Context
-import android.support.v7.widget.RecyclerView
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
-
-open class RVItemClickListener(context: Context, private val itemClickListener: OnItemClickListener?) : RecyclerView.OnItemTouchListener {
-
- private var gestureDetector: GestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
- override fun onSingleTapUp(e: MotionEvent): Boolean {
- return true
- }
- })
+import androidx.recyclerview.widget.RecyclerView
+
+open class RVItemClickListener(
+ context: Context,
+ private val itemClickListener: OnItemClickListener?
+) :
+ RecyclerView.OnItemTouchListener {
+
+ private var gestureDetector: GestureDetector =
+ GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ return true
+ }
+ })
interface OnItemClickListener {
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/SingletonHolder.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/SingletonHolder.kt
index 8054715..0104eac 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/SingletonHolder.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/SingletonHolder.kt
@@ -4,7 +4,8 @@ package com.mentalmachines.droidcon_boston.utils
// Kotlin singletons really should have a constructor :-P
open class SingletonHolder(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
- @Volatile private var instance: T? = null
+ @Volatile
+ private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/TwitterViewModelFactory.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/TwitterViewModelFactory.kt
new file mode 100644
index 0000000..1137a58
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/utils/TwitterViewModelFactory.kt
@@ -0,0 +1,17 @@
+package com.mentalmachines.droidcon_boston.utils
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.mentalmachines.droidcon_boston.data.Repository
+import com.mentalmachines.droidcon_boston.domain.TwitterUseCase
+import com.mentalmachines.droidcon_boston.views.social.TwitterViewModel
+
+class TwitterViewModelFactory(private val context: Context) :
+ ViewModelProvider.NewInstanceFactory() {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class) = TwitterViewModel(
+ TwitterUseCase(Repository
+ .getInstance(context))) as T
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/AboutFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/AboutFragment.kt
index 018d749..4c2239f 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/AboutFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/AboutFragment.kt
@@ -1,26 +1,29 @@
package com.mentalmachines.droidcon_boston.views
import android.os.Bundle
-import android.support.v4.app.Fragment
import android.text.method.LinkMovementMethod
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
import com.mentalmachines.droidcon_boston.utils.getHtmlFormattedSpanned
-import kotlinx.android.synthetic.main.about_fragment.tv_about_description
+import kotlinx.android.synthetic.main.about_fragment.*
+import timber.log.Timber
class AboutFragment : Fragment() {
private val firebaseHelper = FirebaseHelper.instance
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
return inflater.inflate(R.layout.about_fragment, container, false)
}
@@ -35,15 +38,16 @@ class AboutFragment : Fragment() {
firebaseHelper.aboutDatabase.removeEventListener(dataListener)
}
- val dataListener: ValueEventListener = object : ValueEventListener {
+ private val dataListener: ValueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
- tv_about_description.text = dataSnapshot.getValue(String::class.java)?.getHtmlFormattedSpanned()
+ tv_about_description.text =
+ dataSnapshot.getValue(String::class.java)?.getHtmlFormattedSpanned()
tv_about_description.movementMethod = LinkMovementMethod.getInstance()
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "onCancelled", databaseError.toException())
+ Timber.e(databaseError.toException())
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/CocFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/CocFragment.kt
index c889603..9b59257 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/CocFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/CocFragment.kt
@@ -2,25 +2,29 @@ package com.mentalmachines.droidcon_boston.views
import android.os.Bundle
-import android.support.v4.app.Fragment
import android.text.method.LinkMovementMethod
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
import com.mentalmachines.droidcon_boston.utils.getHtmlFormattedSpanned
-import kotlinx.android.synthetic.main.coc_fragment.tv_coc
+import kotlinx.android.synthetic.main.coc_fragment.*
+import timber.log.Timber
class CocFragment : Fragment() {
private val firebaseHelper = FirebaseHelper.instance
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.coc_fragment, container, false)
}
@@ -36,7 +40,7 @@ class CocFragment : Fragment() {
firebaseHelper.cocDatabase.removeEventListener(dataListener)
}
- val dataListener: ValueEventListener = object : ValueEventListener {
+ private val dataListener: ValueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
tv_coc.text = dataSnapshot.getValue(String::class.java)?.getHtmlFormattedSpanned()
@@ -44,11 +48,11 @@ class CocFragment : Fragment() {
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "onCancelled", databaseError.toException())
+ Timber.d(databaseError.toException())
}
}
private fun fetchDataFromFirebase() {
firebaseHelper.cocDatabase.addValueEventListener(dataListener)
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/FAQFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/FAQFragment.kt
index c7720a4..ac26a71 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/FAQFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/FAQFragment.kt
@@ -3,13 +3,11 @@ package com.mentalmachines.droidcon_boston.views
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
import android.text.TextUtils
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
@@ -19,9 +17,9 @@ import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
import com.mentalmachines.droidcon_boston.views.faq.FaqAdapterItem
import com.mentalmachines.droidcon_boston.views.faq.FaqAdapterItemHeader
import eu.davidea.flexibleadapter.FlexibleAdapter
-import kotlinx.android.synthetic.main.faq_fragment.faq_recycler
-import java.util.ArrayList
-import java.util.HashMap
+import kotlinx.android.synthetic.main.faq_fragment.*
+import timber.log.Timber
+import java.util.*
class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
@@ -30,7 +28,11 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
private lateinit var headerAdapter: FlexibleAdapter
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(layout.faq_fragment, container, false)
}
@@ -47,7 +49,7 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
firebaseHelper.faqDatabase.removeEventListener(dataListener)
}
- val dataListener: ValueEventListener = object : ValueEventListener {
+ private val dataListener: ValueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val rows = ArrayList()
for (faqSnapshot in dataSnapshot.children) {
@@ -62,7 +64,7 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "onCancelled", databaseError.toException())
+ Timber.e(databaseError.toException())
}
}
@@ -75,7 +77,8 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
val items = ArrayList(faqs.size)
faqs.forEach { faq ->
faq.answers.forEach { answer ->
- val header: FaqAdapterItemHeader = questionHeaders[faq.question] ?: FaqAdapterItemHeader(faq.question)
+ val header: FaqAdapterItemHeader =
+ questionHeaders[faq.question] ?: FaqAdapterItemHeader(faq.question)
questionHeaders[faq.question] = header
val item = FaqAdapterItem(answer, header)
@@ -83,12 +86,12 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
}
- faq_recycler.layoutManager = LinearLayoutManager(faq_recycler.context)
+ faq_recycler.layoutManager =
+ androidx.recyclerview.widget.LinearLayoutManager(faq_recycler.context)
headerAdapter = FlexibleAdapter(items)
headerAdapter.addListener(this)
faq_recycler.adapter = headerAdapter
- headerAdapter.expandItemsAtStartUp()
- .setDisplayHeadersAtStartUp(true)
+ headerAdapter.expandItemsAtStartUp().setDisplayHeadersAtStartUp(true)
}
override fun onItemClick(view: View, position: Int): Boolean {
@@ -96,7 +99,8 @@ class FAQFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
val item = headerAdapter.getItem(position)
val itemData = item!!.itemData
- val url = if (!TextUtils.isEmpty(itemData.otherLink)) itemData.otherLink else itemData.mapLink
+ val url =
+ if (!TextUtils.isEmpty(itemData.otherLink)) itemData.otherLink else itemData.mapLink
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/MainActivity.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/MainActivity.kt
index 4324785..842f1dd 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/MainActivity.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/MainActivity.kt
@@ -1,78 +1,120 @@
package com.mentalmachines.droidcon_boston.views
+import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v4.view.GravityCompat
-import android.support.v7.app.ActionBarDrawerToggle
-import android.support.v7.app.AppCompatActivity
import android.text.TextUtils
-import android.view.Gravity
import android.view.MenuItem
+import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.GravityCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.R.id
+import com.mentalmachines.droidcon_boston.R.string
import com.mentalmachines.droidcon_boston.data.Schedule.ScheduleRow
+import com.mentalmachines.droidcon_boston.firebase.AuthController
import com.mentalmachines.droidcon_boston.utils.ServiceLocator
import com.mentalmachines.droidcon_boston.views.agenda.AgendaFragment
import com.mentalmachines.droidcon_boston.views.detail.AgendaDetailFragment
+import com.mentalmachines.droidcon_boston.views.search.SearchDialog
import com.mentalmachines.droidcon_boston.views.social.SocialFragment
+import com.mentalmachines.droidcon_boston.views.social.TwitterFragment
import com.mentalmachines.droidcon_boston.views.speaker.SpeakerFragment
import com.mentalmachines.droidcon_boston.views.volunteer.VolunteerFragment
-import kotlinx.android.synthetic.main.main_activity.drawer_layout
-import kotlinx.android.synthetic.main.main_activity.navView
-import kotlinx.android.synthetic.main.main_activity.toolbar
-
+import kotlinx.android.synthetic.main.main_activity.*
class MainActivity : AppCompatActivity() {
private lateinit var actionBarDrawerToggle: ActionBarDrawerToggle
+ private val searchDialog = SearchDialog()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
initNavDrawerToggle()
- replaceFragment(getString(R.string.str_agenda))
+ setInitialFragment(savedInstanceState)
+
+ initFragmentsFromIntent(intent)
+
+ initSearchDialog()
+
+ updateDrawerLoginState()
+ }
+
+ private fun initSearchDialog() {
+ searchDialog.itemClicked = {
+ AgendaDetailFragment.addDetailFragmentToStack(supportFragmentManager, it)
+ }
+ }
- val sessionDetails = intent.extras?.getString(EXTRA_SESSION_DETAILS)
+ private fun setInitialFragment(savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
+ replaceFragment(getString(string.str_agenda))
+ }
+ }
+
+ private fun initFragmentsFromIntent(initialIntent: Intent) {
+ val sessionDetails = initialIntent.extras?.getString(EXTRA_SESSION_DETAILS)
if (!TextUtils.isEmpty(sessionDetails)) {
- AgendaDetailFragment.addDetailFragmentToStack(supportFragmentManager,
- ServiceLocator.gson.fromJson(sessionDetails, ScheduleRow::class.java))
- } else {
- navView.setCheckedItem(R.id.nav_agenda)
+ replaceFragment(getString(string.str_agenda))
+ AgendaDetailFragment.addDetailFragmentToStack(
+ supportFragmentManager,
+ ServiceLocator.gson.fromJson(sessionDetails, ScheduleRow::class.java)
+ )
+ updateSelectedNavItem(supportFragmentManager)
}
}
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+
+ intent?.let {
+ initFragmentsFromIntent(it)
+ }
+ }
override fun onBackPressed() {
// If drawer is open
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
// close the drawer
- drawer_layout.closeDrawer(Gravity.START)
+ drawer_layout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
val manager = supportFragmentManager
if (manager.backStackEntryCount == 0) {
// special handling where user clicks on back button in a detail fragment
- val currentFragment = manager.findFragmentById(R.id.fragment_container)
- if (currentFragment is AgendaFragment) {
- if (currentFragment.isMyAgenda()) {
- checkNavMenuItem(getString(R.string.str_my_schedule))
- } else {
- checkNavMenuItem(getString(R.string.str_agenda))
- }
- } else if (currentFragment is SpeakerFragment) {
- checkNavMenuItem(getString(R.string.str_speakers))
- }
+ updateSelectedNavItem(manager)
}
}
}
+ private fun updateSelectedNavItem(manager: FragmentManager) {
+ val currentFragment = manager.findFragmentById(id.fragment_container)
+ if (currentFragment is AgendaFragment) {
+ if (currentFragment.isMyAgenda()) {
+ checkNavMenuItem(getString(string.str_my_schedule))
+ } else {
+ checkNavMenuItem(getString(string.str_agenda))
+ }
+ } else if (currentFragment is SpeakerFragment) {
+ checkNavMenuItem(getString(string.str_speakers))
+ }
+ }
+
private fun checkNavMenuItem(title: String) {
- processMenuItems({ item -> item.title == title }, { item -> item.setChecked(true).isChecked })
+ processMenuItems(
+ { item -> item.title == title },
+ { item -> item.setChecked(true).isChecked },
+ processAll = true
+ )
}
private fun isNavItemChecked(title: String): Boolean {
@@ -80,56 +122,65 @@ class MainActivity : AppCompatActivity() {
}
fun uncheckAllMenuItems() {
- processMenuItems({ _ -> true }, { item -> item.setChecked(false).isChecked }, true)
+ processMenuItems({ true }, { item -> item.setChecked(false).isChecked }, true)
}
- private fun processMenuItems(titleMatcher: (MenuItem) -> Boolean,
- matchFunc: (MenuItem) -> Boolean,
- processAll: Boolean = false): Boolean {
+ private fun processMenuItems(
+ titleMatcher: (MenuItem) -> Boolean,
+ matchFunc: (MenuItem) -> Boolean,
+ processAll: Boolean = false
+ ): Boolean {
val menu = navView.menu
for (i in 0 until menu.size()) {
val item = menu.getItem(i)
- if (item.hasSubMenu()) {
- val subMenu = item.subMenu
- for (j in 0 until subMenu.size()) {
- val subMenuItem = subMenu.getItem(j)
-
- if (titleMatcher(subMenuItem)) {
- val result = matchFunc(subMenuItem)
- if (!processAll) {
- return result
+ when {
+ item.hasSubMenu() -> {
+ val subMenu = item.subMenu
+ for (j in 0 until subMenu.size()) {
+ val subMenuItem = subMenu.getItem(j)
+
+ if (titleMatcher(subMenuItem)) {
+ val result = matchFunc(subMenuItem)
+ if (!processAll) {
+ return result
+ }
}
}
}
- } else if (titleMatcher(item)) {
- val result = matchFunc(item)
- if (!processAll) {
- return result
+ titleMatcher(item) -> {
+ val result = matchFunc(item)
+ if (!processAll) {
+ return result
+ }
}
+ else -> item.isChecked = false
}
}
- return false
+ return processAll
}
-
private fun initNavDrawerToggle() {
setSupportActionBar(toolbar)
- actionBarDrawerToggle = ActionBarDrawerToggle(this, drawer_layout,
- R.string.drawer_open, R.string.drawer_close)
+ actionBarDrawerToggle = ActionBarDrawerToggle(
+ this,
+ drawer_layout,
+ R.string.drawer_open,
+ R.string.drawer_close
+ )
drawer_layout.addDrawerListener(actionBarDrawerToggle)
navView.setNavigationItemSelectedListener { item ->
- //Closing drawer on item click
+ // Closing drawer on item click
drawer_layout.closeDrawers()
when (item.itemId) {
- // Respond to the action bar's Up/Home button
- android.R.id.home -> if (fragmentManager.backStackEntryCount > 0) {
- fragmentManager.popBackStack()
- } else if (fragmentManager.backStackEntryCount == 1) {
+ // Respond to the action bar's Up/Home button
+ android.R.id.home -> if (supportFragmentManager.backStackEntryCount > 0) {
+ supportFragmentManager.popBackStack()
+ } else if (supportFragmentManager?.backStackEntryCount == 1) {
// to avoid looping below on initScreen
super.onBackPressed()
finish()
@@ -142,9 +193,21 @@ class MainActivity : AppCompatActivity() {
R.id.nav_about -> replaceFragment(getString(R.string.str_about_us))
R.id.nav_speakers -> replaceFragment(getString(R.string.str_speakers))
R.id.nav_volunteers -> replaceFragment(getString(R.string.str_volunteers))
+ R.id.nav_login_logout -> {
+ if (AuthController.isLoggedIn) {
+ logout()
+ } else {
+ login()
+ }
+ }
+ R.id.nav_tweet_feed -> replaceFragment(getString(R.string.str_twitter_feed))
}
- navView.setCheckedItem(item.itemId)
+ if (item.itemId != R.id.nav_login_logout) {
+ navView.setCheckedItem(item.itemId)
+ } else {
+ updateSelectedNavItem(supportFragmentManager)
+ }
true
}
@@ -171,60 +234,54 @@ class MainActivity : AppCompatActivity() {
return if (actionBarDrawerToggle.onOptionsItemSelected(item)) {
true
} else super.onOptionsItemSelected(item)
-
}
private fun replaceFragment(title: String) {
- if (isNavItemChecked(title)) {
- // Fragment currently selected, no action.
- return
- }
-
checkNavMenuItem(title)
-
updateToolbarTitle(title)
// Get the fragment by tag
- var fragment: Fragment? = supportFragmentManager.findFragmentByTag(title)
+ var fragment: androidx.fragment.app.Fragment? =
+ supportFragmentManager.findFragmentByTag(title)
if (fragment == null) {
// Initialize the fragment based on tag
- when (title) {
- resources.getString(R.string.str_agenda) -> fragment = AgendaFragment.newInstance()
- resources.getString(R.string.str_my_schedule) -> fragment = AgendaFragment.newInstanceMySchedule()
- resources.getString(R.string.str_faq) -> fragment = FAQFragment()
- resources.getString(R.string.str_social) -> fragment = SocialFragment()
- resources.getString(R.string.str_coc) -> fragment = CocFragment()
- resources.getString(R.string.str_about_us) -> fragment = AboutFragment()
- resources.getString(R.string.str_speakers) -> fragment = SpeakerFragment()
- resources.getString(R.string.str_volunteers) -> fragment = VolunteerFragment()
- }
- // Add fragment with tag
- supportFragmentManager.beginTransaction().replace(R.id.fragment_container, fragment, title).commit()
+ fragment = createFragmentForTitle(title)
} else {
-
// For Agenda and My Schedule Screen, which add more fragments to backstack.
// Remove all fragment except the last one when navigating via the nav drawer.
when (title) {
- resources.getString(R.string.str_agenda) ->
- popUntilLastFragment()
- resources.getString(R.string.str_my_schedule) ->
- popUntilLastFragment()
+ resources.getString(R.string.str_agenda),
+ resources.getString(R.string.str_my_schedule) -> {
+ supportFragmentManager.popBackStack(
+ title,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE
+ )
+ }
}
+ }
- supportFragmentManager.beginTransaction()
- // detach the fragment that is currently visible
- .detach(supportFragmentManager.findFragmentById(R.id.fragment_container))
- // attach the fragment found as per the tag
- .attach(fragment)
- // commit fragment transaction
- .commit()
+ fragment?.let {
+ supportFragmentManager?.beginTransaction()
+ // replace in container
+ ?.replace(R.id.fragment_container, it, title)
+ // commit fragment transaction
+ ?.commit()
}
}
- private fun popUntilLastFragment() {
- for (i in 0..supportFragmentManager.backStackEntryCount) {
- supportFragmentManager.popBackStack()
+ private fun createFragmentForTitle(title: String): Fragment? {
+ return when (title) {
+ resources.getString(string.str_agenda) -> AgendaFragment.newInstance()
+ resources.getString(string.str_my_schedule) -> AgendaFragment.newInstanceMySchedule()
+ resources.getString(string.str_faq) -> FAQFragment()
+ resources.getString(string.str_social) -> SocialFragment()
+ resources.getString(string.str_coc) -> CocFragment()
+ resources.getString(string.str_about_us) -> AboutFragment()
+ resources.getString(string.str_speakers) -> SpeakerFragment()
+ resources.getString(string.str_volunteers) -> VolunteerFragment()
+ resources.getString(string.str_twitter_feed) -> TwitterFragment.newInstance()
+ else -> null
}
}
@@ -234,16 +291,61 @@ class MainActivity : AppCompatActivity() {
}
}
+ private fun login() {
+ AuthController.login(this, RC_SIGN_IN, R.mipmap.ic_launcher)
+ }
+
+ private fun logout() {
+ AuthController.logout(this) {
+ updateDrawerLoginState()
+ }
+ }
+
+ private fun updateDrawerLoginState() {
+ navView.menu.findItem(R.id.nav_login_logout).title = getString(
+ if (AuthController.isLoggedIn) {
+ R.string.str_logout
+ } else {
+ R.string.str_login
+ }
+ )
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ AuthController.handleLoginResult(this, resultCode, data)?.let {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.str_title_error)
+ .setMessage(it)
+ .show()
+ } ?: run {
+ updateDrawerLoginState()
+ }
+ }
+
+ override fun onSearchRequested(): Boolean {
+ searchDialog.show(supportFragmentManager, SEARCH_DIALOG_TAG)
+ return true
+ }
+
companion object {
private const val EXTRA_SESSIONID = "MainActivity.EXTRA_SESSIONID"
private const val EXTRA_SESSION_DETAILS = "MainActivity.EXTRA_SESSION_DETAILS"
+ private const val SEARCH_DIALOG_TAG = "agenda_search_tag"
+
+ private const val RC_SIGN_IN = 1
- fun getSessionDetailIntent(context: Context, sessionId: String, sessionDetail: String): Intent {
- val intent = Intent(context, MainActivity::class.java).apply {
+ fun getSessionDetailIntent(
+ context: Context,
+ sessionId: String,
+ sessionDetail: String
+ ): Intent {
+ return Intent(context, MainActivity::class.java).apply {
putExtra(EXTRA_SESSIONID, sessionId)
putExtra(EXTRA_SESSION_DETAILS, sessionDetail)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
- return intent
}
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/SplashActivity.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/SplashActivity.kt
index 291d3a1..4bd6605 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/SplashActivity.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/SplashActivity.kt
@@ -3,56 +3,93 @@ package com.mentalmachines.droidcon_boston.views
import android.content.Intent
import android.os.Bundle
import android.os.Handler
-import android.support.v7.app.AppCompatActivity
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
-import com.mentalmachines.droidcon_boston.R
+import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
-import android.view.animation.AlphaAnimation
-import kotlinx.android.synthetic.main.splash_activity.logo_text
-
+import androidx.appcompat.app.AppCompatActivity
+import com.mentalmachines.droidcon_boston.R
class SplashActivity : AppCompatActivity() {
+ lateinit var logoText: View
+ lateinit var logoImage: View
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.splash_activity)
+
+ logoText = findViewById(R.id.logoText)
+ logoImage = findViewById(R.id.logoImage)
+
+ logoImage.translationY = LOGO_START_TRANSLATION_Y
+
+ logoImage.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(v: View?) {}
+
+ override fun onViewAttachedToWindow(v: View?) {
+ logoImage.animate()
+ .translationY(LOGO_END_TRANSLATION_Y)
+ .setDuration(LOGO_ENTER_DURATION)
+ .setInterpolator(AccelerateDecelerateInterpolator())
+ .start()
+ }
+ })
}
override fun onStart() {
super.onStart()
- Handler().postDelayed(this::fadeImage, SPLASH_DURATION)
+ Handler().postDelayed(this::fadeScreenElements, SPLASH_DURATION)
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
- overridePendingTransition(0, 0);
+ overridePendingTransition(0, 0)
finish()
}
- private fun fadeImage() {
- val a = AlphaAnimation(1.00f, 0.00f)
+ private fun fadeScreenElements() {
+ fun createFadeAnimation() = AlphaAnimation(VISIBLE_OPACITY, GONE_OPACITY).apply {
+ interpolator = AccelerateDecelerateInterpolator()
+ duration = FADE_DURATION
+ }
- a.interpolator = AccelerateDecelerateInterpolator()
- a.duration = FADE_DURATION
+ val textAnimation = createFadeAnimation()
+ val logoAnimation = createFadeAnimation()
- a.setAnimationListener(object : AnimationListener {
+ textAnimation.setAnimationListener(object : AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationRepeat(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
- logo_text.setVisibility(View.GONE)
+ logoText.visibility = View.GONE
startMainActivity()
}
})
- logo_text.startAnimation(a)
+ logoAnimation.setAnimationListener(object : AnimationListener {
+ override fun onAnimationStart(animation: Animation) {}
+ override fun onAnimationRepeat(animation: Animation) {}
+ override fun onAnimationEnd(animation: Animation) {
+ logoImage.visibility = View.GONE
+ }
+ })
+
+ logoText.startAnimation(textAnimation)
+ logoImage.startAnimation(logoAnimation)
}
companion object {
- val FADE_DURATION: Long = 750
- val SPLASH_DURATION: Long = 1500
+ const val FADE_DURATION: Long = 750
+ const val SPLASH_DURATION: Long = 2000
+
+ const val VISIBLE_OPACITY = 1.00f
+ const val GONE_OPACITY = 0.00f
+
+ const val LOGO_ENTER_DURATION = 500L
+ const val LOGO_START_TRANSLATION_Y = 1000f
+ const val LOGO_END_TRANSLATION_Y = 0f
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayFragment.kt
index 0d8ff5c..1ddf797 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayFragment.kt
@@ -1,54 +1,109 @@
package com.mentalmachines.droidcon_boston.views.agenda
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.util.Log
+import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.ValueEventListener
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.LinearInterpolator
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearSmoothScroller
+import androidx.recyclerview.widget.RecyclerView
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieDrawable
+import com.google.android.material.button.MaterialButton
import com.mentalmachines.droidcon_boston.R
-import com.mentalmachines.droidcon_boston.data.FirebaseDatabase.ScheduleEvent
-import com.mentalmachines.droidcon_boston.data.Schedule
import com.mentalmachines.droidcon_boston.data.Schedule.ScheduleRow
import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
-import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
-import com.mentalmachines.droidcon_boston.utils.ServiceLocator.Companion.gson
import com.mentalmachines.droidcon_boston.utils.isNullorEmpty
import com.mentalmachines.droidcon_boston.views.detail.AgendaDetailFragment
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.helpers.EmptyViewHelper
-import java.util.*
-
+import timber.log.Timber
/**
* Fragment for an agenda day
*/
class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
private val timeHeaders = HashMap()
+ private val floatAnimation = AnimatorSet()
- private var dayFilter: String = ""
- private val firebaseHelper = FirebaseHelper.instance
- private var onlyMyAgenda: Boolean = false
-
- private lateinit var userAgendaRepo: UserAgendaRepo
- private var headerAdapter: FlexibleAdapter? = null
+ private var headerAdapter: FlexibleAdapter<*>? = null
+ private lateinit var layoutManager: LinearLayoutManager
private lateinit var agendaRecyler: RecyclerView
private lateinit var emptyStateView: View
+ private lateinit var emptyFilterView: View
+ private lateinit var scrollToCurrentButton: MaterialButton
+ private lateinit var viewModel: AgendaDayViewModel
+ private lateinit var agendaProgressView: LottieAnimationView
+
+ private val viewModelFactory = object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ val dayFilter = arguments?.getString(ARG_DAY) ?: ""
+ val onlyMyAgenda = arguments?.getBoolean(ARG_MY_AGENDA) ?: false
+ val userAgendaRepo = UserAgendaRepo.getInstance(requireContext())
+
+ @Suppress("UNCHECKED_CAST")
+ return AgendaDayViewModel(dayFilter, onlyMyAgenda, userAgendaRepo) as T
+ }
+ }
+
+ /**
+ * Total number of sessions that begin after now and end before now.
+ * where 'now' was determined when these items were loaded from Firebase.
+ */
+ private var totalCurrentSessionCount = 0
+
+ /**
+ * Target scroll to position when Jump to current is clicked.
+ */
+ private var targetCurrentSesssionPosition = 0
+
+ /**
+ * Number of sessions that begin after now and end before now, which are currently
+ * attached to the RecyclerView (in the users view).
+ */
+ private var visibleCurrentSessionCount = 0
+ set(value) {
+ field = wrapBounds(value, 0, totalCurrentSessionCount)
+ field = value
+ updateJumpToCurrentButtonVisibility(value > 0)
+ }
+
+ private fun wrapBounds(value: Int, min: Int, max: Int) =
+ if (value >= max) {
+ Timber.w("Value out of bounds value=$value, min=$min, max=$max")
+ max
+ } else if (value < 0) {
+ Timber.w("Value out of bounds value=$value, min=$min, max=$max")
+ min
+ } else {
+ value
+ }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- dayFilter = arguments?.getString(ARG_DAY) ?: ""
- userAgendaRepo = UserAgendaRepo.getInstance(context!!)
+ private fun initViewModel() {
+ viewModel =
+ ViewModelProviders.of(this, viewModelFactory).get(AgendaDayViewModel::class.java)
+
+ viewModel.scheduleRows.observe(viewLifecycleOwner, Observer {
+ it?.let(this::setupHeaderAdapter)
+ })
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
@@ -63,75 +118,165 @@ class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
return super.onOptionsItemSelected(item)
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
return inflater.inflate(R.layout.agenda_day_fragment, container, false)
}
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// NOTE: Kotlin Extensions' agenda_vew is null in setupHeaderAdapter sporadically, so do this old school
agendaRecyler = view.findViewById(R.id.agenda_recycler)
emptyStateView = view.findViewById(R.id.empty_view)
+ emptyFilterView = view.findViewById(R.id.empty_filter_view)
+ scrollToCurrentButton = view.findViewById(R.id.scroll_to_current_session)
+ agendaProgressView = view.findViewById(R.id.speaker_image)
+
+ layoutManager = LinearLayoutManager(requireActivity().applicationContext)
+ agendaRecyler.layoutManager = layoutManager
+
+ val linearSmoothScroller = setupSmoothScroller()
+ addFloatingAnimation()
+ scrollToCurrentButton.setOnClickListener {
+ linearSmoothScroller.targetPosition = targetCurrentSesssionPosition
+ (agendaRecyler.layoutManager as LinearLayoutManager).startSmoothScroll(
+ linearSmoothScroller
+ )
+ }
- agendaRecyler.layoutManager = LinearLayoutManager(activity?.applicationContext)
-
- onlyMyAgenda = arguments?.getBoolean(ARG_MY_AGENDA) ?: false
-
+ initViewModel()
fetchScheduleData()
activity?.supportFragmentManager?.addOnBackStackChangedListener(backStackChangeListener)
}
+ override fun onStart() {
+ super.onStart()
+ agendaProgressView.setAnimation("dancing_droid.json")
+ agendaProgressView.playAnimation()
+ agendaProgressView.repeatCount = LottieDrawable.INFINITE
+ }
+
private val backStackChangeListener: () -> Unit = {
- if (onlyMyAgenda) {
+ if (viewModel.onlyMyAgenda) {
fetchScheduleData()
} else {
headerAdapter?.notifyDataSetChanged()
}
}
+ private fun setupSmoothScroller(): RecyclerView.SmoothScroller {
+ return object : LinearSmoothScroller(context) {
+ override fun getVerticalSnapPreference(): Int {
+ return SNAP_TO_START
+ }
+
+ override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
+ }
+ }
+ }
+
+ private fun updateJumpToCurrentButtonVisibility(isCurrentSessionVisible: Boolean) {
+ if (isCurrentSessionVisible) {
+ fadeOutJumpToCurrentButton()
+ } else {
+ if (totalCurrentSessionCount > 0) {
+ fadeInJumpToCurrentButton()
+ }
+ }
+ }
+
+ private fun fadeOutJumpToCurrentButton() {
+ val viewPropAnimator = scrollToCurrentButton
+ .animate()
+ .alpha(JumpToCurrent.ButtonVisibility.minAlpha)
+ .setDuration(JumpToCurrent.ButtonVisibility.duration)
+ .setInterpolator(DecelerateInterpolator())
+ viewPropAnimator.withEndAction { scrollToCurrentButton.visibility = View.GONE }
+ viewPropAnimator.start()
+ }
+
+ private fun fadeInJumpToCurrentButton() {
+ val viewPropAnimator = scrollToCurrentButton
+ .animate().alpha(JumpToCurrent.ButtonVisibility.maxAlpha)
+ .setDuration(JumpToCurrent.ButtonVisibility.duration)
+ .setInterpolator(DecelerateInterpolator())
+ viewPropAnimator.withStartAction { scrollToCurrentButton.visibility = View.VISIBLE }
+ }
+
+ private fun addFloatingAnimation() {
+
+ // Float up
+ val propertyValuesHolder = PropertyValuesHolder.ofFloat(
+ View.TRANSLATION_Y,
+ JumpToCurrent.ButtonTranslation.translationY,
+ -JumpToCurrent.ButtonTranslation.translationY
+ )
+
+ val floatUpAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ scrollToCurrentButton, propertyValuesHolder
+ )
+ floatUpAnimator.duration = JumpToCurrent.ButtonTranslation.duration
+ floatUpAnimator.interpolator = LinearInterpolator()
+
+ // Float down
+ val downFloatValues = PropertyValuesHolder.ofFloat(
+ View.TRANSLATION_Y,
+ -JumpToCurrent.ButtonTranslation.translationY,
+ JumpToCurrent.ButtonTranslation.translationY
+ )
+ val floatDownAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ scrollToCurrentButton, downFloatValues
+ )
+ floatDownAnimator.duration = JumpToCurrent.ButtonTranslation.duration
+ floatDownAnimator.interpolator = LinearInterpolator()
+
+ floatAnimation.playSequentially(floatUpAnimator, floatDownAnimator)
+ floatAnimation.start()
+ floatAnimation.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ floatAnimation.start()
+ }
+ })
+ }
+
override fun onDestroyView() {
super.onDestroyView()
- firebaseHelper.eventDatabase.removeEventListener(dataListener)
+ floatAnimation.cancel();
+ agendaRecyler.removeOnChildAttachStateChangeListener(currentSessionVisibleListener)
activity?.supportFragmentManager?.removeOnBackStackChangedListener(backStackChangeListener)
}
fun updateList() {
- agendaRecyler.adapter.notifyDataSetChanged()
- }
-
- val dataListener: ValueEventListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val rows = ArrayList()
- for (roomSnapshot in dataSnapshot.children) {
- val key = roomSnapshot.key
- val data = roomSnapshot.getValue(ScheduleEvent::class.java)
- Log.d(TAG, "Event: $data")
- if (data != null) {
- val scheduleRow = data.toScheduleRow(key)
- if (scheduleRow.date == dayFilter && (!onlyMyAgenda
- || onlyMyAgenda && userAgendaRepo.isSessionBookmarked(scheduleRow.id))) {
- rows.add(scheduleRow)
- }
- }
- }
-
- setupHeaderAdapter(rows)
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- Log.w(TAG, "scheduleQuery:onCancelled", databaseError.toException())
- }
+ agendaRecyler.adapter?.notifyDataSetChanged()
}
private fun fetchScheduleData() {
- firebaseHelper.eventDatabase.addValueEventListener(dataListener)
+ viewModel.fetchScheduleData()
}
+ private val currentSessionVisibleListener =
+ object : RecyclerView.OnChildAttachStateChangeListener {
+
+ override fun onChildViewAttachedToWindow(view: View) {
+ if (view.tag == CURRENT_ITEM_MARKER_TAG) {
+ visibleCurrentSessionCount++
+ }
+ }
+
+ override fun onChildViewDetachedFromWindow(view: View) {
+ if (view.tag == CURRENT_ITEM_MARKER_TAG) {
+ visibleCurrentSessionCount--
+ }
+ }
+ }
+
private fun setupHeaderAdapter(rows: List) {
val items = ArrayList(rows.size)
for (row in rows) {
@@ -146,32 +291,59 @@ class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
items.add(item)
}
- val sortedItems = items.sortedWith(
- compareBy { it.itemData.utcStartTimeString }
- .thenBy { it.roomSortOrder })
+ val sortedItems =
+ items.sortedWith(compareBy { it.itemData.utcStartTimeString }
+ .thenBy { it.roomSortOrder })
headerAdapter = FlexibleAdapter(sortedItems)
+ agendaRecyler.addOnChildAttachStateChangeListener(currentSessionVisibleListener)
headerAdapter!!.addListener(this)
agendaRecyler.adapter = headerAdapter
- agendaRecyler.addItemDecoration(FlexibleItemDecoration(agendaRecyler.context).withDefaultDivider())
+ agendaProgressView.visibility = View.GONE
+ agendaRecyler
+ .addItemDecoration(FlexibleItemDecoration(agendaRecyler.context)
+ .withDefaultDivider())
headerAdapter!!.expandItemsAtStartUp().setDisplayHeadersAtStartUp(true)
- EmptyViewHelper(headerAdapter, emptyStateView, null,null)
+ EmptyViewHelper(headerAdapter, emptyStateView, emptyFilterView, null)
+
+ initializeJumpButtonVariables(sortedItems)
+ }
+
+ private fun initializeJumpButtonVariables(sortedItems: List) {
+
+ // Total number of sessions that begin before now and end after now.
+ // where 'now' was determined when these items were loaded from Firebase.
+ totalCurrentSessionCount = sortedItems.count { it.itemData.isCurrentSession }
+
+ if (totalCurrentSessionCount > 0) {
+ // Scroll target when jump to now selected.
+ val currentItems = headerAdapter!!.currentItems
+ val indexOfFirstCurrentSession = currentItems.indexOfFirst {
+ (it is ScheduleAdapterItem) && it.itemData.isCurrentSession
+ }
+ targetCurrentSesssionPosition =
+ minOf(indexOfFirstCurrentSession, currentItems.lastIndex)
+ }
+
+ // Initialize the number of visible current sessions to zero.
+ visibleCurrentSessionCount = 0
}
override fun onItemClick(view: View, position: Int): Boolean {
- val adapterItem = try { headerAdapter?.getItem(position) } catch (e: Exception) { null }
+ val adapterItem = headerAdapter?.getItem(position)
+
if (adapterItem is ScheduleAdapterItem) {
val itemData = adapterItem.itemData
if (itemData.primarySpeakerName.isNullorEmpty()) {
- val url = itemData.photoUrlMap.get(itemData.primarySpeakerName)
+ val url = itemData.photoUrlMap[itemData.primarySpeakerName]
if (!url.isNullorEmpty()) {
// event where info URL is in the photoUrls string
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(url)
val packageManager = activity?.packageManager
- if (i.resolveActivity(packageManager) != null) {
+ if (packageManager != null && i.resolveActivity(packageManager) != null) {
startActivity(i)
}
return false
@@ -179,7 +351,7 @@ class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
activity?.let {
- AgendaDetailFragment.addDetailFragmentToStack(it.supportFragmentManager, itemData);
+ AgendaDetailFragment.addDetailFragmentToStack(it.supportFragmentManager, itemData)
}
}
@@ -187,10 +359,9 @@ class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
companion object {
-
- private val TAG = AgendaDayFragment::class.java.name
private const val ARG_DAY = "day"
private const val ARG_MY_AGENDA = "my_agenda"
+ private const val MILLISECONDS_PER_INCH = 50f
fun newInstance(myAgenda: Boolean, day: String): AgendaDayFragment {
val fragment = AgendaDayFragment()
@@ -201,5 +372,18 @@ class AgendaDayFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
return fragment
}
}
-}
+ object JumpToCurrent {
+
+ object ButtonVisibility {
+ const val minAlpha = 0.0f
+ const val maxAlpha = 1.0f
+ const val duration = 750L
+ }
+
+ object ButtonTranslation {
+ const val duration = 3000L
+ const val translationY = 30.0f
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayPagerAdapter.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayPagerAdapter.kt
index 2baf820..d9dd65b 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayPagerAdapter.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayPagerAdapter.kt
@@ -1,22 +1,33 @@
package com.mentalmachines.droidcon_boston.views.agenda
-import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import com.mentalmachines.droidcon_boston.BuildConfig
import com.mentalmachines.droidcon_boston.data.Schedule
-class AgendaDayPagerAdapter internal constructor(fm: FragmentManager, private val myAgenda: Boolean)
- : FixedFragmentStatePagerAdapter(fm) {
+class AgendaDayPagerAdapter internal constructor(
+ fm: FragmentManager,
+ private val myAgenda: Boolean
+) :
+ FixedFragmentStatePagerAdapter(fm) {
- private val PAGE_COUNT = 2
+ private val pageCount = 2
private val tabTitles = arrayOf("Day 1", "Day 2")
override fun getCount(): Int {
- return PAGE_COUNT
+ return pageCount
}
override fun getItem(position: Int): Fragment {
- return AgendaDayFragment.newInstance(myAgenda,
- if (position == 0) Schedule.MONDAY else Schedule.TUESDAY
+ val dayString = if (position == 0) {
+ BuildConfig.EVENT_DAY_ONE_STRING
+ } else {
+ BuildConfig.EVENT_DAY_TWO_STRING
+ }
+
+ return AgendaDayFragment.newInstance(
+ myAgenda,
+ dayString
)
}
@@ -33,4 +44,4 @@ class AgendaDayPagerAdapter internal constructor(fm: FragmentManager, private va
override fun getFragmentItem(position: Int): Fragment {
return getItem(position)
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayViewModel.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayViewModel.kt
new file mode 100644
index 0000000..446019b
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaDayViewModel.kt
@@ -0,0 +1,79 @@
+package com.mentalmachines.droidcon_boston.views.agenda
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.ViewModel
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.ValueEventListener
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import com.mentalmachines.droidcon_boston.data.Schedule
+import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
+import timber.log.Timber
+
+class AgendaDayViewModel(
+ private val dayFilter: String,
+ val onlyMyAgenda: Boolean,
+ private val userAgendaRepo: UserAgendaRepo
+) : ViewModel() {
+ private val firebaseHelper = FirebaseHelper.instance
+
+ private val _scheduleRows = MutableLiveData>()
+ private val _activeFilter = MutableLiveData()
+
+ /**
+ * Whenever an active filter is set, we filter out the [_scheduleRows] for any that contain the
+ * filter within the title, description, or speaker names.
+ */
+ val scheduleRows: LiveData> =
+ Transformations.map(_activeFilter) { constraint ->
+ _scheduleRows.value?.filter { itemData ->
+ itemData.containsKeyword(constraint)
+ }
+ }
+
+ private val dataListener: ValueEventListener = object : ValueEventListener {
+ override fun onDataChange(dataSnapshot: DataSnapshot) {
+ val rows = ArrayList()
+ for (roomSnapshot in dataSnapshot.children) {
+ val key = roomSnapshot.key ?: ""
+ val data = roomSnapshot.getValue(FirebaseDatabase.ScheduleEvent::class.java)
+ Timber.d("Event: $data")
+ if (data != null) {
+ val scheduleRow = data.toScheduleRow(key)
+ val matchesDay = scheduleRow.date == dayFilter
+ val isPublicView = !onlyMyAgenda
+ val isPrivateAndBookmarked = onlyMyAgenda && userAgendaRepo
+ .isSessionBookmarked(scheduleRow.id)
+
+ if (matchesDay && (isPublicView || isPrivateAndBookmarked)) {
+ rows.add(scheduleRow)
+ }
+ }
+ }
+
+ _scheduleRows.value = rows
+ _activeFilter.value = ""
+ }
+
+ override fun onCancelled(databaseError: DatabaseError) {
+ Timber.e(databaseError.toException())
+ }
+ }
+
+ fun fetchScheduleData() {
+ firebaseHelper.eventDatabase.removeEventListener(dataListener)
+ firebaseHelper.eventDatabase.addValueEventListener(dataListener)
+ }
+
+ fun setActiveFilter(filter: String) {
+ _activeFilter.value = filter
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ firebaseHelper.eventDatabase.removeEventListener(dataListener)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaFragment.kt
index 5be9cc6..7dadf21 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/AgendaFragment.kt
@@ -1,32 +1,40 @@
package com.mentalmachines.droidcon_boston.views.agenda
import android.os.Bundle
-import android.support.v4.app.Fragment
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.mentalmachines.droidcon_boston.BuildConfig
import com.mentalmachines.droidcon_boston.R
-import kotlinx.android.synthetic.main.agenda_fragment.tablayout
-import kotlinx.android.synthetic.main.agenda_fragment.viewpager
-import java.util.Calendar
+import kotlinx.android.synthetic.main.agenda_fragment.*
+import java.util.*
class AgendaFragment : Fragment() {
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
- return inflater.inflate(R.layout.agenda_fragment, container, false)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
}
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.agenda_fragment, container, false)
+ }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupDayPager(savedInstanceState)
}
-
private fun setupDayPager(savedInstanceState: Bundle?) {
- viewpager.adapter = AgendaDayPagerAdapter(childFragmentManager,
- isMyAgenda())
+ viewpager.adapter = AgendaDayPagerAdapter(childFragmentManager, isMyAgenda())
tablayout.setupWithViewPager(viewpager)
@@ -36,7 +44,11 @@ class AgendaFragment : Fragment() {
// set current day to second if today matches
val today = Calendar.getInstance()
val dayTwo = Calendar.getInstance()
- dayTwo.set(2018, Calendar.MARCH, 27)
+ dayTwo.set(
+ BuildConfig.EVENT_YEAR,
+ BuildConfig.EVENT_MONTH - 1, // Calendar is 0 indexed
+ BuildConfig.EVENT_DAY_TWO
+ )
if (today == dayTwo) {
viewpager.currentItem = 1
}
@@ -46,8 +58,24 @@ class AgendaFragment : Fragment() {
fun isMyAgenda() = arguments?.getBoolean(ARG_MY_AGENDA) ?: false
override fun onSaveInstanceState(outState: Bundle) {
+ tablayout?.let {
+ outState.putInt(TAB_POSITION, tablayout.selectedTabPosition)
+ }
super.onSaveInstanceState(outState)
- outState.putInt(TAB_POSITION, tablayout.selectedTabPosition)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
+ inflater?.inflate(R.menu.menu_search, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+ return when (item?.itemId) {
+ R.id.search -> {
+ activity?.onSearchRequested()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
}
companion object {
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/FixedFragmentStatePagerAdapter.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/FixedFragmentStatePagerAdapter.kt
index aa12260..4fc40fe 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/FixedFragmentStatePagerAdapter.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/FixedFragmentStatePagerAdapter.kt
@@ -1,12 +1,11 @@
package com.mentalmachines.droidcon_boston.views.agenda
-import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
-import android.support.v4.app.FragmentStatePagerAdapter
-import android.support.v4.view.PagerAdapter
import android.view.ViewGroup
-
-import java.util.WeakHashMap
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentStatePagerAdapter
+import androidx.viewpager.widget.PagerAdapter
+import java.util.*
/**
* See https://stackoverflow.com/questions/13695649/refresh-images-on-fragmentstatepageradapter-on-resuming-activity
@@ -54,14 +53,12 @@ abstract class FixedFragmentStatePagerAdapter(fm: FragmentManager) : FragmentSta
/**
* Find the location of a fragment in the hashmap if it being view
- * @param object the Fragment we want to check for
+ * @param fragmentObj the Fragment we want to check for
* @return the position if found else -1
*/
private fun findFragmentPositionHashMap(fragmentObj: Fragment): Int {
for (position in mFragments.keys) {
- if (position != null &&
- mFragments[position] != null &&
- mFragments[position] === fragmentObj) {
+ if (position != null && mFragments[position] != null && mFragments[position] === fragmentObj) {
return position
}
}
@@ -71,4 +68,4 @@ abstract class FixedFragmentStatePagerAdapter(fm: FragmentManager) : FragmentSta
abstract fun getFragmentItem(position: Int): Fragment
abstract fun updateFragmentItem(position: Int, fragment: Fragment)
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItem.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItem.kt
index f610a86..94bc734 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItem.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItem.kt
@@ -1,12 +1,11 @@
package com.mentalmachines.droidcon_boston.views.agenda
-import android.support.v4.content.ContextCompat
-import android.support.v7.widget.RecyclerView
-import android.util.Log
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.data.Schedule
@@ -16,16 +15,21 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
+import timber.log.Timber
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
+const val CURRENT_ITEM_MARKER_TAG = "CURRENT_ITEM_MARKER_TAG"
+
/**
* Used for displaying the schedule with sticky headers with optional day filtering
*/
-class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRow,
- header: ScheduleAdapterItemHeader) :
- AbstractSectionableItem(header) {
+class ScheduleAdapterItem internal constructor(
+ val itemData: Schedule.ScheduleRow,
+ header: ScheduleAdapterItemHeader
+) :
+ AbstractSectionableItem(header) {
private var startTime: Date = Date()
@@ -40,7 +44,7 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
try {
startTime = format.parse(dateTimeString)
} catch (e: ParseException) {
- Log.e("ScheduleAdapterItem", "Parse error: $e for $dateTimeString")
+ Timber.e("Parse error: $e for $dateTimeString")
}
}
@@ -61,14 +65,19 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
return R.layout.schedule_item
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return ScheduleAdapterItem.ViewHolder(view, adapter)
}
- override fun bindViewHolder(adapter: FlexibleAdapter>?,
- holder: ViewHolder,
- position: Int,
- payloads: MutableList) {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>?,
+ holder: ViewHolder,
+ position: Int,
+ payloads: MutableList
+ ) {
val userAgendaRepo = UserAgendaRepo.getInstance(holder.bookmarkIndicator.context)
if (itemData.speakerNames.isEmpty()) {
@@ -77,12 +86,12 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
holder.speaker.visibility = View.GONE
holder.time.visibility = View.GONE
- holder.bookmarkIndicator.visibility = if (userAgendaRepo.isSessionBookmarked(itemData.id))
- View.VISIBLE
- else
- View.INVISIBLE
+ holder.bookmarkIndicator.visibility =
+ if (userAgendaRepo.isSessionBookmarked(itemData.id)) View.VISIBLE
+ else View.INVISIBLE
holder.sessionLayout.visibility = View.VISIBLE
holder.title.text = itemData.talkTitle
+
holder.room.text = itemData.room
if (itemData.photoUrlMap.size == 0) {
@@ -102,28 +111,36 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
holder.speaker.text = itemData.speakerNames.joinToString(separator = ", ")
holder.room.text = itemData.room
- holder.speakerCount.visibility = if (itemData.speakerCount > 1) View.VISIBLE else View.GONE
+ holder.speakerCount.visibility =
+ if (itemData.speakerCount > 1) View.VISIBLE else View.GONE
holder.speakerCount.text = String.format("+%d", itemData.speakerCount - 1)
val context = holder.title.context
- Glide.with(context)
- .load(itemData.photoUrlMap[itemData.primarySpeakerName])
- .transform(CircleTransform(context))
- .placeholder(R.drawable.emo_im_cool)
- .crossFade()
- .into(holder.avatar)
+ Glide.with(context).load(itemData.photoUrlMap[itemData.primarySpeakerName])
+ .transform(CircleTransform(context)).placeholder(R.drawable.emo_im_cool).crossFade()
+ .into(holder.avatar)
- holder.bookmarkIndicator.visibility = if (userAgendaRepo.isSessionBookmarked(itemData.id))
- View.VISIBLE
- else
- View.INVISIBLE
+ holder.bookmarkIndicator.visibility =
+ if (userAgendaRepo.isSessionBookmarked(itemData.id)) View.VISIBLE
+ else View.INVISIBLE
addBackgroundRipple(holder)
}
val availableColor = if (itemData.isOver) R.color.colorGray else R.color.colorAccent
- holder.availableIndicator.setBackgroundColor(ContextCompat.getColor(holder.availableIndicator.context, availableColor))
+ holder.availableIndicator.setBackgroundColor(
+ ContextCompat.getColor(
+ holder.availableIndicator.context,
+ availableColor
+ )
+ )
+
+ if (itemData.isCurrentSession) {
+ holder.root.tag = CURRENT_ITEM_MARKER_TAG
+ } else {
+ holder.root.tag = null
+ }
}
private fun addBackgroundRipple(holder: ViewHolder) {
@@ -136,6 +153,8 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
class ViewHolder : FlexibleViewHolder {
+ lateinit var root: View
+
lateinit var rootLayout: View
lateinit var availableIndicator: ImageView
@@ -163,12 +182,17 @@ class ScheduleAdapterItem internal constructor(val itemData: Schedule.ScheduleRo
findViews(view)
}
- constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(view, adapter, stickyHeader) {
+ constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(
+ view,
+ adapter,
+ stickyHeader
+ ) {
findViews(view)
}
private fun findViews(parent: View) {
+ root = parent
rootLayout = parent.findViewById(R.id.scheduleRootLayout)
availableIndicator = parent.findViewById(R.id.available_indicator)
bookmarkIndicator = parent.findViewById(R.id.bookmark_indicator)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItemHeader.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItemHeader.kt
index f922809..557567a 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItemHeader.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/agenda/ScheduleAdapterItemHeader.kt
@@ -1,8 +1,8 @@
package com.mentalmachines.droidcon_boston.views.agenda
-import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
import com.mentalmachines.droidcon_boston.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@@ -12,8 +12,8 @@ import eu.davidea.viewholders.FlexibleViewHolder
/**
* Sticky header for schedule view
*/
-class ScheduleAdapterItemHeader internal constructor(private val sessionTime: String)
- : AbstractHeaderItem() {
+class ScheduleAdapterItemHeader internal constructor(private val sessionTime: String) :
+ AbstractHeaderItem() {
override fun equals(other: Any?): Boolean {
if (other is ScheduleAdapterItemHeader) {
@@ -31,29 +31,35 @@ class ScheduleAdapterItemHeader internal constructor(private val sessionTime: St
return R.layout.schedule_item_header
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return ScheduleAdapterItemHeader.ViewHolder(view, adapter, true)
}
- override fun bindViewHolder(adapter: FlexibleAdapter>,
- holder: ScheduleAdapterItemHeader.ViewHolder,
- position: Int,
- payloads: MutableList) {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: ScheduleAdapterItemHeader.ViewHolder,
+ position: Int,
+ payloads: MutableList
+ ) {
holder.header.text = sessionTime
}
-
class ViewHolder : FlexibleViewHolder {
lateinit var header: TextView
- constructor(view: View, adapter: FlexibleAdapter<*>)
- : super(view, adapter) {
+ constructor(view: View, adapter: FlexibleAdapter<*>) : super(view, adapter) {
findViews(view)
}
- internal constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean)
- : super(view, adapter, stickyHeader) {
+ internal constructor(
+ view: View,
+ adapter: FlexibleAdapter<*>,
+ stickyHeader: Boolean
+ ) : super(view, adapter, stickyHeader) {
findViews(view)
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailFragment.kt
index 7fca566..e044dab 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailFragment.kt
@@ -1,22 +1,25 @@
package com.mentalmachines.droidcon_boston.views.detail
+import android.app.Activity
+import android.content.Intent
import android.content.res.ColorStateList
+import android.net.Uri
import android.os.Bundle
-import android.support.design.widget.Snackbar
-import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
-import android.support.v4.content.ContextCompat
import android.text.method.LinkMovementMethod
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProviders
import com.bumptech.glide.Glide
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.ValueEventListener
+import com.google.android.material.snackbar.Snackbar
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.R.string
import com.mentalmachines.droidcon_boston.data.FirebaseDatabase.EventSpeaker
@@ -24,37 +27,49 @@ import com.mentalmachines.droidcon_boston.data.Schedule
import com.mentalmachines.droidcon_boston.data.Schedule.ScheduleDetail
import com.mentalmachines.droidcon_boston.data.Schedule.ScheduleRow
import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
+import com.mentalmachines.droidcon_boston.firebase.AuthController
import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
import com.mentalmachines.droidcon_boston.utils.NotificationUtils
import com.mentalmachines.droidcon_boston.utils.ServiceLocator.Companion.gson
import com.mentalmachines.droidcon_boston.utils.getHtmlFormattedSpanned
+import com.mentalmachines.droidcon_boston.utils.visibleIf
import com.mentalmachines.droidcon_boston.views.MainActivity
+import com.mentalmachines.droidcon_boston.views.rating.RatingDialog
+import com.mentalmachines.droidcon_boston.views.rating.RatingRepo
import com.mentalmachines.droidcon_boston.views.transform.CircleTransform
-import kotlinx.android.synthetic.main.agenda_detail_fragment.agendaDetailView
-import kotlinx.android.synthetic.main.agenda_detail_fragment.fab_agenda_detail_bookmark
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_description
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_room
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_speaker_name
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_speaker_title
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_time
-import kotlinx.android.synthetic.main.agenda_detail_fragment.tv_agenda_detail_title
-import kotlinx.android.synthetic.main.agenda_detail_fragment.v_agenda_detail_speaker_divider
-
+import kotlinx.android.synthetic.main.agenda_detail_fragment.*
class AgendaDetailFragment : Fragment() {
- private var scheduleDetail: ScheduleDetail? = null
- private lateinit var scheduleRowItem: ScheduleRow
- private val eventSpeakers = HashMap()
+ val ratingRepo = RatingRepo(AuthController.userId.orEmpty(), FirebaseHelper.instance.userDatabase)
- private val firebaseHelper = FirebaseHelper.instance
+ private val viewModelFactory = object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ val scheduleRowItem = gson.fromJson(
+ arguments?.getString(Schedule.SCHEDULE_ITEM_ROW),
+ ScheduleRow::class.java
+ )
- private val userAgendaRepo: UserAgendaRepo
- get() = UserAgendaRepo.getInstance(fab_agenda_detail_bookmark.context)
+ val userAgendaRepo = UserAgendaRepo.getInstance(requireContext())
+ @Suppress("UNCHECKED_CAST")
+ return AgendaDetailViewModel(scheduleRowItem, userAgendaRepo, ratingRepo) as T
+ }
+ }
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ private lateinit var viewModel: AgendaDetailViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = false
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.agenda_detail_fragment, container, false)
}
@@ -62,81 +77,91 @@ class AgendaDetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- scheduleRowItem = gson.fromJson(arguments!!.getString(Schedule.SCHEDULE_ITEM_ROW), ScheduleRow::class.java)
- fetchDataFromFirebase()
+ initializeViewModel()
+ loadData()
populateView()
- if (activity is MainActivity) {
- val mainActivity = activity as MainActivity
- mainActivity.uncheckAllMenuItems()
- }
+ (activity as? MainActivity)?.uncheckAllMenuItems()
}
- private fun populateView() {
- tv_agenda_detail_title.text = scheduleRowItem.talkTitle
- tv_agenda_detail_room.text = resources.getString(R.string.str_agenda_detail_room, scheduleRowItem.room)
- tv_agenda_detail_time.text = resources.getString(R.string.str_agenda_detail_time, scheduleRowItem.startTime, scheduleRowItem.endTime)
-
- fab_agenda_detail_bookmark.setOnClickListener({
-
- if (scheduleDetail != null) {
- val nextBookmarkStatus = !userAgendaRepo.isSessionBookmarked(scheduleDetail!!.id)
- userAgendaRepo.bookmarkSession(scheduleDetail!!.id, nextBookmarkStatus)
- val context = tv_agenda_detail_title.context
- if (nextBookmarkStatus) {
- NotificationUtils(context).scheduleMySessionNotifications()
- } else {
- NotificationUtils(context).cancelNotificationAlarm(scheduleRowItem.id)
- }
+ private fun loadData() {
+ viewModel.loadData()
+ }
- Snackbar.make(agendaDetailView,
- if (nextBookmarkStatus)
- getString(R.string.saved_agenda_item)
- else getString(R.string.removed_agenda_item),
- Snackbar.LENGTH_SHORT).show()
+ private fun initializeViewModel() {
+ viewModel =
+ ViewModelProviders.of(this, viewModelFactory).get(AgendaDetailViewModel::class.java)
- showBookmarkStatus(scheduleDetail!!)
- }
+ viewModel.scheduleDetail.observe(viewLifecycleOwner, Observer {
+ it?.let(this::showAgendaDetail)
})
- populateSpeakersInformation(scheduleRowItem)
+ viewModel.ratingValue.observe(viewLifecycleOwner, Observer {
+ it?.let{
+ session_rating.rating = it.toFloat()
+ }
+ })
}
- val dataListener: ValueEventListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- for (speakerSnapshot in dataSnapshot.children) {
- val speaker = speakerSnapshot.getValue(EventSpeaker::class.java)
- if (speaker != null) {
- eventSpeakers.put(speaker.name, speaker)
+ private fun populateView() {
+ tv_agenda_detail_title.text = viewModel.talkTitle
- if (scheduleRowItem.primarySpeakerName == speaker.name) {
- scheduleDetail = speaker.toScheduleDetail(scheduleRowItem)
- showAgendaDetail(scheduleDetail!!)
- }
- }
+ tv_agenda_detail_room.text = resources.getString(R.string.str_agenda_detail_room, viewModel.room)
+ tv_agenda_detail_time.text = resources.getString(
+ R.string.str_agenda_detail_time,
+ viewModel.startTime,
+ viewModel.endTime
+ )
+
+ fab_agenda_detail_bookmark.setOnClickListener {
+ viewModel.toggleBookmark()
+
+ if (viewModel.isBookmarked) {
+ NotificationUtils(requireContext()).scheduleMySessionNotifications()
+ } else {
+ NotificationUtils(requireContext()).cancelNotificationAlarm(viewModel.schedulerowId)
}
+ Snackbar.make(
+ agendaDetailView,
+ viewModel.bookmarkSnackbarRes,
+ Snackbar.LENGTH_SHORT
+ ).show()
+
+ showBookmarkStatus()
}
- override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "detailQuery:onCancelled", databaseError.toException())
+ session_rating_overlay.setOnClickListener {
+ if (AuthController.isLoggedIn) {
+ showRatingDialog()
+ } else {
+ AuthController.login(this, RC_SIGN_IN_FEEDBACK, R.mipmap.ic_launcher)
+ }
}
+
+ populateSpeakersInformation()
}
- private fun fetchDataFromFirebase() {
- firebaseHelper.speakerDatabase.orderByChild("name")
- .addValueEventListener(dataListener)
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == RC_SIGN_IN_FEEDBACK && resultCode == Activity.RESULT_OK) {
+ ratingRepo.userId = AuthController.userId.orEmpty()
+ loadData()
+
+ showRatingDialog()
+ }
+
+ super.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroyView() {
super.onDestroyView()
- firebaseHelper.speakerDatabase.removeEventListener(dataListener)
+ viewModel.removeListener()
}
- private fun populateSpeakersInformation(itemData: ScheduleRow) = when {
- itemData.speakerNames.isEmpty() -> {
+ private fun populateSpeakersInformation() = when {
+ viewModel.speakerNames.isEmpty() -> {
tv_agenda_detail_speaker_name.visibility = View.GONE
v_agenda_detail_speaker_divider.visibility = View.GONE
}
@@ -147,32 +172,31 @@ class AgendaDetailFragment : Fragment() {
val offsetImgView = resources.getDimension(R.dimen.imgv_speaker_offset).toInt()
val defaultLeftMargin = resources.getDimension(R.dimen.def_margin).toInt()
- itemData.speakerNames.forEach { speakerName ->
- val orgName: String? = itemData.speakerNameToOrgName[speakerName]
+ viewModel.speakerNames.forEach { speakerName ->
+ val orgName: String? = viewModel.getOrganizationForSpeaker(speakerName)
// append org name to speaker name
speakerNames += speakerName + when {
orgName != null -> " - $orgName"
else -> {
- // Do nothing
+ ' '
}
}
- if (itemData.speakerNames.size > 1) {
+ if (viewModel.speakerNames.size > 1) {
tv_agenda_detail_speaker_title.text = getString(string.header_speakers)
// if the current speaker name is not the last then add a line break
- if (speakerName != itemData.speakerNames.last()) {
+ if (speakerName != viewModel.speakerNames.last()) {
speakerNames += "\n"
}
} else {
tv_agenda_detail_speaker_title.text = getString(string.header_speaker)
}
-
// Add an imageview to the relative layout
val tempImg = ImageView(activity)
val lp = RelativeLayout.LayoutParams(imgViewSize, imgViewSize)
- if (speakerName == itemData.speakerNames.first()) {
+ if (speakerName == viewModel.speakerNames.first()) {
lp.setMargins(marginValue, 0, 0, defaultLeftMargin)
} else {
marginValue += offsetImgView
@@ -181,69 +205,96 @@ class AgendaDetailFragment : Fragment() {
// add the imageview above the textview for room data
lp.addRule(RelativeLayout.ABOVE, tv_agenda_detail_room.id)
+ lp.addRule(RelativeLayout.BELOW, session_rating_overlay.id)
tempImg.layoutParams = lp
// add speakerName as a child to the relative layout
agendaDetailView.addView(tempImg)
- Glide.with(this)
- .load(itemData.photoUrlMap[speakerName])
- .transform(CircleTransform(tempImg.context))
- .placeholder(R.drawable.emo_im_cool)
- .crossFade()
- .into(tempImg)
+ Glide.with(requireContext())
+ .load(viewModel.getPhotoForSpeaker(speakerName))
+ .transform(CircleTransform(tempImg.context))
+ .placeholder(R.drawable.emo_im_cool)
+ .crossFade()
+ .into(tempImg)
- tempImg.setOnClickListener { _ ->
- val eventSpeaker = eventSpeakers[speakerName]
+ tempImg.setOnClickListener {
+ val eventSpeaker = viewModel.getSpeaker(speakerName)
val arguments = Bundle()
- arguments.putString(EventSpeaker.SPEAKER_ITEM_ROW, gson.toJson(eventSpeaker, EventSpeaker::class.java))
+ arguments.putString(
+ EventSpeaker.SPEAKER_ITEM_ROW,
+ gson.toJson(eventSpeaker, EventSpeaker::class.java)
+ )
val speakerDetailFragment = SpeakerDetailFragment()
speakerDetailFragment.arguments = arguments
val fragmentManager = activity?.supportFragmentManager
+
fragmentManager?.beginTransaction()
- ?.add(R.id.fragment_container, speakerDetailFragment)
- ?.addToBackStack(null)
- ?.commit()
+ ?.add(R.id.fragment_container, speakerDetailFragment)
+ ?.addToBackStack(null)
+ ?.commit()
}
}
+
tv_agenda_detail_speaker_name.text = speakerNames
}
}
- fun showAgendaDetail(scheduleDetail: ScheduleDetail) {
- populateSpeakersInformation(scheduleDetail.listRow)
- showBookmarkStatus(scheduleDetail)
+ private fun showAgendaDetail(scheduleDetail: ScheduleDetail) {
+ populateSpeakersInformation()
+ showBookmarkStatus()
tv_agenda_detail_title.text = scheduleDetail.listRow.talkTitle
- tv_agenda_detail_description.text = scheduleDetail.listRow.talkDescription.getHtmlFormattedSpanned()
+ tv_agenda_detail_description.text =
+ scheduleDetail.listRow.talkDescription.getHtmlFormattedSpanned()
tv_agenda_detail_description.movementMethod = LinkMovementMethod.getInstance()
+ tv_agenda_detail_shareText.setOnClickListener {
+ val twitterVal = viewModel.getTwitterHandleForAllSpeakers(scheduleDetail)
+
+ val tweetUrl =
+ "https://twitter.com/intent/tweet?text=I really enjoyed this %23droidconbos talk \"${scheduleDetail.listRow.talkTitle}\" by $twitterVal!"
+ val uri = Uri.parse(tweetUrl)
+ val shareIntent = Intent(Intent.ACTION_VIEW, uri)
+ startActivity(shareIntent)
+ }
+
+ tv_agenda_detail_shareText.visibleIf(scheduleDetail.listRow.speakerCount > 0)
}
- private fun showBookmarkStatus(scheduleDetail: ScheduleDetail) {
- val userAgendaRepo = userAgendaRepo
- val context = fab_agenda_detail_bookmark.context
- fab_agenda_detail_bookmark.backgroundTintList = if (userAgendaRepo.isSessionBookmarked(scheduleDetail.id))
- ColorStateList.valueOf(ContextCompat.getColor(context, R.color.colorAccent))
- else
- ColorStateList.valueOf(ContextCompat.getColor(context, R.color.colorLightGray))
+ private fun showBookmarkStatus() {
+ val color = ContextCompat.getColor(requireContext(), viewModel.bookmarkColorRes)
+
+ fab_agenda_detail_bookmark.backgroundTintList = ColorStateList.valueOf(color)
+ }
+
+ private fun showRatingDialog() {
+ RatingDialog.newInstance(viewModel.schedulerowId)
+ .show(fragmentManager, RATE_DIALOG_TAG)
}
companion object {
- fun addDetailFragmentToStack(supportFragmentManager: FragmentManager, itemData: Schedule.ScheduleRow) {
+ private const val RATE_DIALOG_TAG = "RATE_DIALOG"
+ private const val RC_SIGN_IN_FEEDBACK = 2
+
+ fun addDetailFragmentToStack(
+ supportFragmentManager: FragmentManager,
+ itemData: Schedule.ScheduleRow
+ ) {
val arguments = Bundle()
- arguments.putString(Schedule.SCHEDULE_ITEM_ROW, gson.toJson(itemData, ScheduleRow::class.java))
+ arguments.putString(
+ Schedule.SCHEDULE_ITEM_ROW,
+ gson.toJson(itemData, ScheduleRow::class.java)
+ )
val agendaDetailFragment = AgendaDetailFragment()
agendaDetailFragment.arguments = arguments
supportFragmentManager.beginTransaction()
- ?.add(R.id.fragment_container, agendaDetailFragment)
- ?.addToBackStack(null)
- ?.commit()
+ .add(R.id.fragment_container, agendaDetailFragment).addToBackStack(null).commit()
}
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModel.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModel.kt
new file mode 100644
index 0000000..e74ef88
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModel.kt
@@ -0,0 +1,140 @@
+package com.mentalmachines.droidcon_boston.views.detail
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.ValueEventListener
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import com.mentalmachines.droidcon_boston.data.Schedule
+import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
+import com.mentalmachines.droidcon_boston.views.rating.RatingRepo
+import timber.log.Timber
+
+class AgendaDetailViewModel(
+ private val scheduleRowItem: Schedule.ScheduleRow,
+ private val userAgendaRepo: UserAgendaRepo,
+ private val ratingRepo: RatingRepo,
+ private val firebaseHelper: FirebaseHelper = FirebaseHelper.instance
+) : ViewModel() {
+ private val eventSpeakers: HashMap = hashMapOf()
+
+ private val _scheduleDetail = MutableLiveData()
+ val scheduleDetail: LiveData = _scheduleDetail
+
+ private val _ratingValue = MutableLiveData()
+ val ratingValue: LiveData = _ratingValue
+
+ private val speakerDataListener: ValueEventListener = object : ValueEventListener {
+ override fun onCancelled(databaseError: DatabaseError) {
+ Timber.e(databaseError.toException())
+ }
+
+ override fun onDataChange(databaseSnapshot: DataSnapshot) {
+ databaseSnapshot.children.forEach { speakerSnapshot ->
+ val speaker = speakerSnapshot.getValue(FirebaseDatabase.EventSpeaker::class.java)
+ speaker?.let {
+ eventSpeakers[speaker.name] = speaker
+
+ if (scheduleRowItem.primarySpeakerName == speaker.name) {
+ _scheduleDetail.value = speaker.toScheduleDetail(scheduleRowItem)
+ }
+ }
+ }
+ }
+ }
+
+ val talkTitle: String
+ get() = scheduleRowItem.talkTitle
+
+ val room: String?
+ get() = scheduleRowItem.room
+
+ val startTime: String
+ get() = scheduleRowItem.startTime
+
+ val endTime: String
+ get() = scheduleRowItem.endTime
+
+ val schedulerowId: String
+ get() = scheduleRowItem.id
+
+ val speakerNames: List
+ get() = scheduleRowItem.speakerNames
+
+ private val sessionId: String
+ get() = scheduleDetail.value?.id.orEmpty()
+
+ val isBookmarked: Boolean
+ get() {
+ return sessionId.isNotEmpty() && userAgendaRepo.isSessionBookmarked(sessionId)
+ }
+
+ val bookmarkSnackbarRes: Int
+ get() = if (isBookmarked) R.string.saved_agenda_item else R.string.removed_agenda_item
+
+ val bookmarkColorRes: Int
+ get() = if (isBookmarked) R.color.colorAccent else R.color.colorLightGray
+
+ fun loadData() {
+ if (scheduleRowItem.hasSpeaker()) {
+ // need to get speaker social media links
+ firebaseHelper.speakerDatabase.orderByChild("name").addValueEventListener(speakerDataListener)
+ } else {
+ _scheduleDetail.value = Schedule.ScheduleDetail(scheduleRowItem)
+ }
+
+ ratingRepo.getSessionFeedback(scheduleRowItem.id) { sessionFeedback ->
+ sessionFeedback?.let {
+ _ratingValue.value = it.rating
+ }
+ }
+ }
+
+ fun removeListener() {
+ firebaseHelper.speakerDatabase.removeEventListener(speakerDataListener)
+ }
+
+ fun getSpeaker(speakerName: String): FirebaseDatabase.EventSpeaker? {
+ return eventSpeakers[speakerName]
+ }
+
+ fun getOrganizationForSpeaker(speakerName: String): String? {
+ return scheduleRowItem.speakerNameToOrgName[speakerName]
+ }
+
+ fun getPhotoForSpeaker(speakerName: String): String? {
+ return scheduleRowItem.photoUrlMap[speakerName]
+ }
+
+ /**
+ * Returns the twitter handle for a user if we have one, otherwise returns the speaker name.
+ */
+ private fun getTwitterHandleForSpeaker(speakerName: String): String {
+ val handle = eventSpeakers[speakerName]?.socialProfiles?.get("twitter")
+
+ return if (handle.isNullOrEmpty()) {
+ speakerName
+ } else {
+ "@$handle"
+ }
+ }
+
+ /**
+ * Builds the twitter handle string for multiple speakers.
+ */
+ fun getTwitterHandleForAllSpeakers(detail: Schedule.ScheduleDetail): String {
+ return detail.listRow.speakerNames.joinToString(
+ separator = " and ",
+ transform = this::getTwitterHandleForSpeaker
+ )
+ }
+
+ fun toggleBookmark() {
+ val nextBookmarkStatus = !isBookmarked
+ userAgendaRepo.bookmarkSession(sessionId, nextBookmarkStatus)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/SpeakerDetailFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/SpeakerDetailFragment.kt
index 374bdfb..6b41f6b 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/SpeakerDetailFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/detail/SpeakerDetailFragment.kt
@@ -1,7 +1,6 @@
package com.mentalmachines.droidcon_boston.views.detail
import android.os.Bundle
-import android.support.v4.app.Fragment
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
@@ -15,20 +14,18 @@ import com.mentalmachines.droidcon_boston.utils.getHtmlFormattedSpanned
import com.mentalmachines.droidcon_boston.utils.loadUriInCustomTab
import com.mentalmachines.droidcon_boston.views.MainActivity
import com.mentalmachines.droidcon_boston.views.transform.CircleTransform
-import kotlinx.android.synthetic.main.speaker_detail_fragment.imgv_linkedin
-import kotlinx.android.synthetic.main.speaker_detail_fragment.imgv_speaker_detail_avatar
-import kotlinx.android.synthetic.main.speaker_detail_fragment.imgv_twitter
-import kotlinx.android.synthetic.main.speaker_detail_fragment.tv_speaker_detail_description
-import kotlinx.android.synthetic.main.speaker_detail_fragment.tv_speaker_detail_designation
-import kotlinx.android.synthetic.main.speaker_detail_fragment.tv_speaker_detail_name
+import kotlinx.android.synthetic.main.speaker_detail_fragment.*
-class SpeakerDetailFragment : Fragment() {
+class SpeakerDetailFragment : androidx.fragment.app.Fragment() {
private val firebaseHelper = FirebaseHelper.instance
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.speaker_detail_fragment, container, false)
}
@@ -36,7 +33,10 @@ class SpeakerDetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val itemData = gson.fromJson(arguments!!.getString(EventSpeaker.SPEAKER_ITEM_ROW), EventSpeaker::class.java)
+ val itemData = gson.fromJson(
+ arguments!!.getString(EventSpeaker.SPEAKER_ITEM_ROW),
+ EventSpeaker::class.java
+ )
populateView(itemData)
if (activity is MainActivity) {
@@ -47,16 +47,23 @@ class SpeakerDetailFragment : Fragment() {
private fun populateView(itemData: EventSpeaker) {
tv_speaker_detail_name.text = itemData.name
- tv_speaker_detail_designation.text = String.format("%s \n@ %s", itemData.title, itemData.org)
+ tv_speaker_detail_designation.text =
+ String.format("%s \n@ %s", itemData.title, itemData.org)
tv_speaker_detail_description.text = itemData.bio.getHtmlFormattedSpanned()
tv_speaker_detail_description.movementMethod = LinkMovementMethod.getInstance()
val twitterHandle = itemData.socialProfiles?.get("twitter")
if (!twitterHandle.isNullOrEmpty()) {
- imgv_twitter.setOnClickListener({
- activity?.loadUriInCustomTab(String.format("%s%s", resources.getString(R.string.twitter_link), twitterHandle))
- })
+ imgv_twitter.setOnClickListener {
+ activity?.loadUriInCustomTab(
+ String.format(
+ "%s%s",
+ resources.getString(R.string.twitter_link),
+ twitterHandle
+ )
+ )
+ }
} else {
imgv_twitter.visibility = View.GONE
}
@@ -64,19 +71,22 @@ class SpeakerDetailFragment : Fragment() {
val linkedinHandle = itemData.socialProfiles?.get("linkedIn")
if (!linkedinHandle.isNullOrEmpty()) {
- imgv_linkedin.setOnClickListener({
- activity?.loadUriInCustomTab(String.format("%s%s", resources.getString(R.string.linkedin_profile_link), linkedinHandle))
- })
+ imgv_linkedin.setOnClickListener {
+ activity?.loadUriInCustomTab(
+ String.format(
+ "%s%s",
+ resources.getString(R.string.linkedin_profile_link),
+ linkedinHandle
+ )
+ )
+ }
} else {
imgv_linkedin.visibility = View.GONE
}
- Glide.with(activity)
- .load(itemData.pictureUrl)
- .transform(CircleTransform(imgv_speaker_detail_avatar.context))
- .placeholder(R.drawable.emo_im_cool)
- .crossFade()
- .into(imgv_speaker_detail_avatar)
+ Glide.with(activity).load(itemData.pictureUrl)
+ .transform(CircleTransform(imgv_speaker_detail_avatar.context))
+ .placeholder(R.drawable.emo_im_cool).crossFade().into(imgv_speaker_detail_avatar)
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItem.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItem.kt
index 6c14a1d..7ee51d5 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItem.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItem.kt
@@ -1,6 +1,5 @@
package com.mentalmachines.droidcon_boston.views.faq
-import android.support.v7.widget.RecyclerView
import android.text.TextUtils
import android.util.TypedValue
import android.view.View
@@ -17,9 +16,8 @@ import eu.davidea.viewholders.FlexibleViewHolder
/**
* Used for displaying the FAQ items
*/
-class FaqAdapterItem internal constructor(val itemData: Answer,
- header: FaqAdapterItemHeader) :
- AbstractSectionableItem(header) {
+class FaqAdapterItem internal constructor(val itemData: Answer, header: FaqAdapterItemHeader) :
+ AbstractSectionableItem(header) {
override fun equals(other: Any?): Boolean {
if (other is FaqAdapterItem) {
@@ -37,26 +35,28 @@ class FaqAdapterItem internal constructor(val itemData: Answer,
return R.layout.faq_item
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return FaqAdapterItem.ViewHolder(view, adapter)
}
- override fun bindViewHolder(adapter: FlexibleAdapter>,
- holder: FaqAdapterItem.ViewHolder,
- position: Int,
- payloads: MutableList) {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: FaqAdapterItem.ViewHolder,
+ position: Int,
+ payloads: MutableList
+ ) {
- holder.faq_text.text = itemData.answer
+ holder.faqText.text = itemData.answer
if (!TextUtils.isEmpty(itemData.photoLink)) {
- val context = holder.faq_text.context
- Glide.with(context)
- .load(itemData.photoLink)
- .crossFade()
- .centerCrop()
- .into(holder.faq_photo)
- holder.faq_photo.visibility = View.VISIBLE
+ val context = holder.faqText.context
+ Glide.with(context).load(itemData.photoLink).crossFade().centerCrop()
+ .into(holder.faqPhoto)
+ holder.faqPhoto.visibility = View.VISIBLE
} else {
- holder.faq_photo.visibility = View.GONE
+ holder.faqPhoto.visibility = View.GONE
}
if (!TextUtils.isEmpty(itemData.otherLink) || !TextUtils.isEmpty(itemData.mapLink)) {
@@ -66,24 +66,24 @@ class FaqAdapterItem internal constructor(val itemData: Answer,
private fun addBackgroundRipple(holder: ViewHolder) {
val outValue = TypedValue()
- val context = holder.faq_text.context
+ val context = holder.faqText.context
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
- holder.root_layout.setBackgroundResource(outValue.resourceId)
+ holder.rootLayout.setBackgroundResource(outValue.resourceId)
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
- lateinit var root_layout: View
+ lateinit var rootLayout: View
- lateinit var faq_text: TextView
+ lateinit var faqText: TextView
- lateinit var faq_photo: ImageView
+ lateinit var faqPhoto: ImageView
private fun findViews(parent: View) {
- root_layout = parent.findViewById(R.id.root_layout)
- faq_text = parent.findViewById(R.id.faq_text)
- faq_photo = parent.findViewById(R.id.faq_photo)
+ rootLayout = parent.findViewById(R.id.root_layout)
+ faqText = parent.findViewById(R.id.faq_text)
+ faqPhoto = parent.findViewById(R.id.faq_photo)
}
init {
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItemHeader.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItemHeader.kt
index 25b62a2..015db71 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItemHeader.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/faq/FaqAdapterItemHeader.kt
@@ -1,8 +1,8 @@
package com.mentalmachines.droidcon_boston.views.faq
-import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
import com.mentalmachines.droidcon_boston.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@@ -12,8 +12,8 @@ import eu.davidea.viewholders.FlexibleViewHolder
/**
* Header for FAQ view
*/
-class FaqAdapterItemHeader internal constructor(private val question: String)
- : AbstractHeaderItem() {
+class FaqAdapterItemHeader internal constructor(private val question: String) :
+ AbstractHeaderItem() {
override fun equals(other: Any?): Boolean {
if (other is FaqAdapterItemHeader) {
@@ -31,14 +31,19 @@ class FaqAdapterItemHeader internal constructor(private val question: String)
return R.layout.faq_header
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return FaqAdapterItemHeader.ViewHolder(view, adapter, true)
}
- override fun bindViewHolder(adapter: FlexibleAdapter>,
- holder: FaqAdapterItemHeader.ViewHolder,
- position: Int,
- payloads: List<*>) {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: FaqAdapterItemHeader.ViewHolder,
+ position: Int,
+ payloads: List<*>
+ ) {
holder.header.text = question
}
@@ -47,13 +52,15 @@ class FaqAdapterItemHeader internal constructor(private val question: String)
lateinit var header: TextView
- constructor(view: View, adapter: FlexibleAdapter<*>)
- : super(view, adapter) {
+ constructor(view: View, adapter: FlexibleAdapter<*>) : super(view, adapter) {
findViews(view)
}
- internal constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean)
- : super(view, adapter, stickyHeader) {
+ internal constructor(
+ view: View,
+ adapter: FlexibleAdapter<*>,
+ stickyHeader: Boolean
+ ) : super(view, adapter, stickyHeader) {
findViews(view)
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingDialog.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingDialog.kt
new file mode 100644
index 0000000..9439b08
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingDialog.kt
@@ -0,0 +1,116 @@
+package com.mentalmachines.droidcon_boston.views.rating
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import com.google.android.material.textfield.TextInputEditText
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.firebase.AuthController
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
+import com.mentalmachines.droidcon_boston.views.MainActivity
+
+class RatingDialog : DialogFragment() {
+ private var sessionRatingBar: RatingBar? = null
+ private var sessionFeedbackInput: TextInputEditText? = null
+ private var cancelButton: Button? = null
+ private var submitButton: Button? = null
+
+ private lateinit var viewModel: RatingViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(R.layout.dialog_rating, container, false)
+
+ initializeViewModel()
+ findViews(view)
+ setupClickListeners()
+ loadPreviousFeedback()
+
+ return view
+ }
+
+ private fun initializeViewModel() {
+ viewModel = ViewModelProviders.of(this).get(RatingViewModel::class.java)
+
+ // should not be able to get to this dialog w/o being logged in
+ val userId = if (AuthController.userId != null) AuthController.userId else ' '
+ val ratingRepo = RatingRepo(userId.toString(), FirebaseHelper.instance.userDatabase)
+ viewModel.init(userId.toString(), ratingRepo)
+
+ viewModel.getFeedbackSent().observe(viewLifecycleOwner, Observer {
+ this.dismiss()
+ })
+ }
+
+ private fun findViews(view: View) {
+ sessionRatingBar = view.findViewById(R.id.session_rating)
+ sessionFeedbackInput = view.findViewById(R.id.session_feedback)
+ cancelButton = view.findViewById(R.id.cancel)
+ submitButton = view.findViewById(R.id.submit)
+ }
+
+ private fun setupClickListeners() {
+ cancelButton?.setOnClickListener {
+ this.dismiss()
+ }
+
+ submitButton?.setOnClickListener {
+ onSubmitClicked()
+ }
+
+ sessionRatingBar?.setOnRatingBarChangeListener { _, rating, _ ->
+ val enableSubmissions = rating > 0.0F
+ submitButton?.isEnabled = enableSubmissions
+ }
+ }
+
+ private fun loadPreviousFeedback() {
+ val sessionId = arguments?.getString(ARG_SESSION_ID).orEmpty()
+ viewModel.getPreviousFeedback(sessionId) {
+ it?.let {
+ sessionFeedbackInput?.setText(it.feedback, TextView.BufferType.NORMAL)
+ sessionRatingBar?.rating = it.rating.toFloat()
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val width = ViewGroup.LayoutParams.MATCH_PARENT
+ val height = ViewGroup.LayoutParams.WRAP_CONTENT
+ dialog?.window?.setLayout(width, height)
+ }
+
+ private fun onSubmitClicked() {
+ val rating = sessionRatingBar?.rating?.toInt() ?: 0
+ val feedback = sessionFeedbackInput?.text?.toString().orEmpty()
+ val sessionId = arguments?.getString(ARG_SESSION_ID).orEmpty()
+
+ viewModel.handleSubmission(rating, feedback, sessionId)
+ }
+
+ companion object {
+ private const val ARG_SESSION_ID = "sessionId"
+
+ fun newInstance(sessionId: String): RatingDialog {
+ val args = Bundle().apply {
+ putString(ARG_SESSION_ID, sessionId)
+ }
+
+ return RatingDialog().apply {
+ arguments = args
+ }
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingRepo.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingRepo.kt
new file mode 100644
index 0000000..d81eefc
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingRepo.kt
@@ -0,0 +1,44 @@
+package com.mentalmachines.droidcon_boston.views.rating
+
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.DatabaseReference
+import com.google.firebase.database.ValueEventListener
+import timber.log.Timber
+
+class RatingRepo(
+ var userId: String,
+ private val userDatabase: DatabaseReference
+) {
+ fun getSessionFeedback(sessionId: String, feedbackCallback: (SessionFeedback?) -> Unit) {
+ userDatabase.child(getSessionFeedbackPath(sessionId))
+ .addValueEventListener(object: ValueEventListener {
+ override fun onCancelled(error: DatabaseError) {
+ Timber.e(error.toException())
+ feedbackCallback(null)
+ }
+
+ override fun onDataChange(data: DataSnapshot) {
+ var rating: Int = 0
+ var comments: String = ""
+ data.children.forEach {
+ when (it.key) {
+ "rating" -> rating = (it.value as Long).toInt()
+ "feedback" -> comments = it.value as String
+ }
+ }
+ feedbackCallback(SessionFeedback(rating, comments))
+ }
+
+ })
+ }
+
+ fun setSessionFeedback(sessionId: String, sessionRating: SessionFeedback) {
+ userDatabase.child(getSessionFeedbackPath(sessionId))
+ .setValue(sessionRating)
+ }
+
+ private fun getSessionFeedbackPath(sessionId: String): String {
+ return "$userId/sessionFeedback/$sessionId"
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModel.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModel.kt
new file mode 100644
index 0000000..18b4533
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModel.kt
@@ -0,0 +1,42 @@
+package com.mentalmachines.droidcon_boston.views.rating
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import timber.log.Timber
+
+class RatingViewModel : ViewModel() {
+
+ private val feedbackSent = MutableLiveData()
+
+ private lateinit var userId: String
+ private lateinit var ratingRepo: RatingRepo
+
+ fun getFeedbackSent(): LiveData = feedbackSent
+
+ fun handleSubmission(rating: Int, feedback: String, sessionId: String) {
+ val sessionFeedback = SessionFeedback(rating, feedback)
+
+ submitFeedback(sessionId, sessionFeedback)
+ }
+
+ /**
+ * Posts the session feedback to the server.
+ */
+ private fun submitFeedback(sessionId: String, sessionFeedback: SessionFeedback) {
+ ratingRepo.setSessionFeedback(sessionId, sessionFeedback)
+
+ Timber.d("Submitting feedback: $sessionFeedback")
+ feedbackSent.value = true
+ }
+
+ fun init(userId: String, ratingRepo: RatingRepo): RatingViewModel {
+ this.userId = userId
+ this.ratingRepo = ratingRepo
+ return this
+ }
+
+ fun getPreviousFeedback(sessionId: String, feedbackCallback: (SessionFeedback?) -> Unit) {
+ ratingRepo.getSessionFeedback(sessionId, feedbackCallback)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/SessionFeedback.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/SessionFeedback.kt
new file mode 100644
index 0000000..189ca23
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/rating/SessionFeedback.kt
@@ -0,0 +1,6 @@
+package com.mentalmachines.droidcon_boston.views.rating
+
+data class SessionFeedback(
+ val rating: Int,
+ val feedback: String
+)
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/ScheduleSearchAdapter.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/ScheduleSearchAdapter.kt
new file mode 100644
index 0000000..7fc7308
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/ScheduleSearchAdapter.kt
@@ -0,0 +1,83 @@
+package com.mentalmachines.droidcon_boston.views.search
+
+import android.content.Context
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Filter
+import android.widget.TextView
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.data.Schedule
+
+class ScheduleSearchAdapter(
+ context: Context,
+ private val layoutRes: Int,
+ private val scheduleRows: List
+) : ArrayAdapter(context, layoutRes, scheduleRows) {
+ private val suggestions: MutableList = mutableListOf()
+ private val tempItems: MutableList = scheduleRows.toMutableList()
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view = convertView ?: LayoutInflater.from(context).inflate(layoutRes, parent, false)
+
+ val scheduleRow = getItem(position)
+ view.findViewById(R.id.talk_title).text = scheduleRow?.talkTitle.orEmpty()
+ @Suppress("DEPRECATION")
+ view.findViewById(R.id.talk_description).text =
+ Html.fromHtml(scheduleRow?.talkDescription)
+
+ return view
+ }
+
+ override fun getItem(position: Int): Schedule.ScheduleRow? {
+ return scheduleRows[position]
+ }
+
+ override fun getCount(): Int {
+ return scheduleRows.size
+ }
+
+ override fun getFilter(): Filter {
+ return object : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ val keyword = constraint ?: return FilterResults()
+
+ suggestions.clear()
+
+ tempItems.forEach {
+ if (it.containsKeyword(keyword.toString())) {
+ suggestions.add(it)
+ }
+ }
+
+ return FilterResults().apply {
+ values = suggestions
+ count = suggestions.size
+ }
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ val tempValues = (results?.values as? List<*>)
+ ?.filterIsInstance(Schedule.ScheduleRow::class.java)
+
+ clear()
+
+ tempValues?.forEach(this@ScheduleSearchAdapter::add)
+
+ notifyDataSetChanged()
+ }
+
+ /**
+ * Since our search dropdown includes actual talks, and not topics the user can suggest,
+ * we don't want to persist that inside the AutoCompleteTextView after all. We want
+ * to make it easy for them to click search and begin typing in the new thing, so we can
+ * just use an empty string here.
+ */
+ override fun convertResultToString(resultValue: Any?): CharSequence {
+ return ""
+ }
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchDialog.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchDialog.kt
new file mode 100644
index 0000000..71183c8
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchDialog.kt
@@ -0,0 +1,113 @@
+package com.mentalmachines.droidcon_boston.views.search
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AutoCompleteTextView
+import android.widget.ImageView
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.data.Schedule
+import com.mentalmachines.droidcon_boston.utils.visibleIf
+
+/**
+ * A full screen dialog that is used to provide searching functionality in the app.
+ */
+class SearchDialog : DialogFragment() {
+ private var backButton: ImageView? = null
+ private var clearButton: ImageView? = null
+ private var searchInput: AutoCompleteTextView? = null
+
+ var itemClicked: ((Schedule.ScheduleRow) -> Unit)? = null
+
+ private lateinit var viewModel: SearchViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialogStyle)
+ viewModel = ViewModelProviders.of(this).get(SearchViewModel::class.java)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.dialog_search, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ backButton = view.findViewById(R.id.action_back)
+ clearButton = view.findViewById(R.id.action_clear)
+ searchInput = view.findViewById(R.id.search_input)
+
+ setupViewListeners()
+ listenForSchedule()
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val width = ViewGroup.LayoutParams.MATCH_PARENT
+ val height = ViewGroup.LayoutParams.MATCH_PARENT
+ dialog?.window?.setLayout(width, height)
+ }
+
+ private fun listenForSchedule() {
+ viewModel.scheduleRows.observe(viewLifecycleOwner, Observer(this::setupSearchAdapter))
+ }
+
+ private fun setupSearchAdapter(suggestions: List?) {
+ val adapter = ScheduleSearchAdapter(
+ requireContext(),
+ R.layout.list_item_schedule_search,
+ suggestions.orEmpty()
+ )
+
+ searchInput?.setAdapter(adapter)
+ }
+
+ /**
+ * Sets all the button click and text listeners relevant to the views within our search dialog.
+ */
+ private fun setupViewListeners() {
+ backButton?.setOnClickListener {
+ dismiss()
+ }
+
+ clearButton?.setOnClickListener {
+ searchInput?.setText("")
+ }
+
+ searchInput?.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {
+ // Not Needed
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ // Not Needed
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ clearButton?.visibleIf(s?.isNotEmpty())
+ }
+ })
+
+ searchInput?.setOnItemClickListener { _, _, position, _ ->
+ val adapter = (searchInput?.adapter as? ScheduleSearchAdapter)
+ val item = adapter?.getItem(position)
+ item?.let {
+ itemClicked?.invoke(it)
+ dismiss()
+ }
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchViewModel.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchViewModel.kt
new file mode 100644
index 0000000..a338fe1
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/search/SearchViewModel.kt
@@ -0,0 +1,52 @@
+package com.mentalmachines.droidcon_boston.views.search
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.ValueEventListener
+import com.mentalmachines.droidcon_boston.BuildConfig
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import com.mentalmachines.droidcon_boston.data.Schedule
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
+import timber.log.Timber
+
+class SearchViewModel : ViewModel() {
+ private val firebaseHelper = FirebaseHelper.instance
+
+ private val _scheduleRows = MutableLiveData>()
+ val scheduleRows: LiveData> = _scheduleRows
+
+ private val dataListener: ValueEventListener = object : ValueEventListener {
+ override fun onDataChange(dataSnapshot: DataSnapshot) {
+ val rows = ArrayList()
+ for (roomSnapshot in dataSnapshot.children) {
+ val key = roomSnapshot.key ?: ""
+ val data = roomSnapshot.getValue(FirebaseDatabase.ScheduleEvent::class.java)
+ if (data != null) {
+ val scheduleRow = data.toScheduleRow(key)
+
+ if (scheduleRow.date.endsWith(BuildConfig.EVENT_YEAR.toString())) {
+ rows.add(scheduleRow)
+ }
+ }
+ }
+
+ _scheduleRows.value = rows
+ }
+
+ override fun onCancelled(databaseError: DatabaseError) {
+ Timber.e(databaseError.toException())
+ }
+ }
+
+ init {
+ firebaseHelper.eventDatabase.addValueEventListener(dataListener)
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ firebaseHelper.eventDatabase.removeEventListener(dataListener)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/RVSocialListAdapter.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/RVSocialListAdapter.kt
index fb1a840..940c7cb 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/RVSocialListAdapter.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/RVSocialListAdapter.kt
@@ -1,26 +1,26 @@
package com.mentalmachines.droidcon_boston.views.social
-import android.support.v7.widget.RecyclerView.Adapter
-import android.support.v7.widget.RecyclerView.ViewHolder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.modal.SocialModal
-import java.util.ArrayList
+import java.util.*
-internal class RVSocialListAdapter(private var socialList: ArrayList) : Adapter() {
+internal class RVSocialListAdapter(private var socialList: ArrayList) :
+ Adapter() {
inner class ListViewHolder(itemView: View) : ViewHolder(itemView) {
- internal var imageView: ImageView
+ internal var imageView: ImageView = itemView.findViewById(R.id.social_item_img)
internal var txtView: TextView
init {
- imageView = itemView.findViewById(R.id.social_item_img)
txtView = itemView.findViewById(R.id.social_item_tv)
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/SocialFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/SocialFragment.kt
index a68cc80..6071fc6 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/SocialFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/SocialFragment.kt
@@ -1,11 +1,11 @@
package com.mentalmachines.droidcon_boston.views.social
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.R.string
import com.mentalmachines.droidcon_boston.modal.SocialModal
@@ -13,14 +13,18 @@ import com.mentalmachines.droidcon_boston.utils.DividerItemDecoration
import com.mentalmachines.droidcon_boston.utils.RVItemClickListener
import com.mentalmachines.droidcon_boston.utils.RVItemClickListener.OnItemClickListener
import com.mentalmachines.droidcon_boston.utils.loadUriInCustomTab
-import kotlinx.android.synthetic.main.social_fragment.social_rv
-import java.util.ArrayList
+import kotlinx.android.synthetic.main.social_fragment.*
+import java.util.*
class SocialFragment : Fragment() {
private lateinit var socialList: ArrayList
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.social_fragment, container, false)
}
@@ -30,33 +34,65 @@ class SocialFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
// Set Layout Manager
- social_rv.layoutManager = LinearLayoutManager(activity)
+ social_rv.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(activity)
// Set the divider
- social_rv.addItemDecoration(DividerItemDecoration(activity!!, LinearLayoutManager.VERTICAL))
+ social_rv.addItemDecoration(
+ DividerItemDecoration(
+ activity!!,
+ androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+ )
+ )
socialList = prepareSocialList()
// Set Adapter
social_rv.adapter = RVSocialListAdapter(socialList)
// Set On Click
- social_rv.addOnItemTouchListener(RVItemClickListener(context!!, object : OnItemClickListener {
- override fun onItemClick(view: View, position: Int) {
- context?.loadUriInCustomTab(socialList[position].link.toString())
- }
- }))
+ social_rv.addOnItemTouchListener(
+ RVItemClickListener(requireContext(),
+ object : OnItemClickListener {
+ override fun onItemClick(view: View, position: Int) {
+ context?.loadUriInCustomTab(socialList[position].link.toString())
+ }
+ })
+ )
}
private fun prepareSocialList(): ArrayList {
val socialList = ArrayList()
- socialList.add(SocialModal(R.drawable.social_facebook, getString(R.string.social_title_facebook),
- getString(R.string.facebook_link)))
- socialList.add(SocialModal(R.drawable.social_instagram, getString(R.string.social_title_instagram),
- getString(R.string.instagram_link)))
- socialList.add(SocialModal(R.drawable.social_linkedin, getString(R.string.social_title_linkedin),
- getString(R.string.linkedin_link)))
- socialList.add(SocialModal(R.drawable.social_twitter, getString(R.string.social_title_twitter),
- String.format("%s%s", resources.getString(R.string.twitter_link), getString(string.droidconbos_twitter_handle))))
+ socialList.add(
+ SocialModal(
+ R.drawable.social_facebook,
+ getString(R.string.social_title_facebook),
+ getString(R.string.facebook_link)
+ )
+ )
+ socialList.add(
+ SocialModal(
+ R.drawable.social_instagram,
+ getString(R.string.social_title_instagram),
+ getString(R.string.instagram_link)
+ )
+ )
+ socialList.add(
+ SocialModal(
+ R.drawable.social_linkedin,
+ getString(R.string.social_title_linkedin),
+ getString(R.string.linkedin_link)
+ )
+ )
+ socialList.add(
+ SocialModal(
+ R.drawable.social_twitter,
+ getString(R.string.social_title_twitter),
+ String.format(
+ "%s%s",
+ resources.getString(R.string.twitter_link),
+ getString(string.droidconbos_twitter_handle)
+ )
+ )
+ )
return socialList
}
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TweetsDiffCallback.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TweetsDiffCallback.kt
new file mode 100644
index 0000000..2c3cdf8
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TweetsDiffCallback.kt
@@ -0,0 +1,14 @@
+package com.mentalmachines.droidcon_boston.views.social
+
+import androidx.recyclerview.widget.DiffUtil
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+class TweetsDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: TweetWithMedia, newItem: TweetWithMedia): Boolean {
+ return oldItem.tweet.id == newItem.tweet.id
+ }
+
+ override fun areContentsTheSame(oldItem: TweetWithMedia, newItem: TweetWithMedia): Boolean {
+ return oldItem.tweet == newItem.tweet
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterFragment.kt
new file mode 100644
index 0000000..de0c316
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterFragment.kt
@@ -0,0 +1,81 @@
+package com.mentalmachines.droidcon_boston.views.social
+
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.ViewModelProviders
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.modal.Result
+import com.mentalmachines.droidcon_boston.utils.TwitterViewModelFactory
+import kotlinx.android.synthetic.main.twitter_fragment.*
+import timber.log.Timber
+
+class TwitterFragment : Fragment(), TwitterRecyclerViewAdapter.OnMediaClickListener {
+
+ companion object {
+ fun newInstance() = TwitterFragment()
+ }
+
+ private val twitterRecyclerViewAdapter: TwitterRecyclerViewAdapter by lazy {
+ TwitterRecyclerViewAdapter(this)
+ }
+
+ private val twitterViewModelFactory: TwitterViewModelFactory by lazy {
+ TwitterViewModelFactory(requireContext())
+ }
+
+ private val twitterViewModel: TwitterViewModel by lazy {
+ ViewModelProviders.of(this, twitterViewModelFactory).get(TwitterViewModel::class.java)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.twitter_fragment, container, false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ recyclerView.adapter = twitterRecyclerViewAdapter
+ swipeToRefreshLayout.setColorSchemeResources(R.color.colorPrimary)
+ twitterViewModel.tweets.observe(this, Observer {
+ when (it) {
+ is Result.Loading -> {
+ if (!swipeToRefreshLayout.isRefreshing) {
+ swipeToRefreshLayout.isRefreshing = true
+ }
+ }
+ is Result.Error -> {
+ swipeToRefreshLayout.isRefreshing = false
+ errorView.visibility = View.VISIBLE
+ recyclerView.visibility = View.GONE
+ Timber.e(it.message)
+ }
+ is Result.Data -> {
+ swipeToRefreshLayout.isRefreshing = false
+ errorView.visibility = View.GONE
+ recyclerView.visibility = View.VISIBLE
+ twitterRecyclerViewAdapter.submitList(it.data)
+ }
+ }
+ })
+ swipeToRefreshLayout.setOnRefreshListener {
+ twitterViewModel.refreshTweets()
+ }
+ }
+
+ override fun onVideoOrGifClick(url: String) {
+ val webIntent: Intent = Uri.parse(url).let { webpage ->
+ Intent(Intent.ACTION_VIEW, webpage)
+ }
+ startActivity(webIntent)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterRecyclerViewAdapter.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterRecyclerViewAdapter.kt
new file mode 100644
index 0000000..4ce9821
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterRecyclerViewAdapter.kt
@@ -0,0 +1,204 @@
+package com.mentalmachines.droidcon_boston.views.social
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.modal.Media
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+import com.mentalmachines.droidcon_boston.views.transform.CircleTransform
+import kotlinx.android.synthetic.main.quoted_tweet_item.view.*
+import kotlinx.android.synthetic.main.tweet_item_layout.view.*
+import kotlinx.android.synthetic.main.twitter_four_image_layout.view.*
+import kotlinx.android.synthetic.main.twitter_gif_layout.view.*
+import kotlinx.android.synthetic.main.twitter_one_image_layout.view.*
+import kotlinx.android.synthetic.main.twitter_three_image_layout.view.*
+import kotlinx.android.synthetic.main.twitter_two_image_layout.view.*
+import kotlinx.android.synthetic.main.twitter_video_layout.view.*
+
+class TwitterRecyclerViewAdapter(private val onMediaClickListener: OnMediaClickListener) :
+ ListAdapter(TweetsDiffCallback()) {
+
+ interface OnMediaClickListener {
+ fun onVideoOrGifClick(url: String)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ viewType, parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position), onMediaClickListener)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return getItem(position).tweet.type
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ fun bind(tweet: TweetWithMedia, onMediaClickListener: OnMediaClickListener) {
+ itemView.run {
+ if (tweet.tweet.type == R.layout.tweet_item_layout) {
+ // Removing '_normal' in profile image url because it's a low resolution image and
+ // will look blurry. There is no alternative solution for this and twitter
+ // recommends this. Url after removing `_normal` gives high resolution image.
+ Glide.with(context).load(
+ tweet.tweet.profileImageUrl.replace(
+ "_normal",
+ ""
+ )
+ ).transform(CircleTransform(context)).crossFade().into(profileImage)
+ screenName.text = tweet.tweet.screenName
+ name.text = String.format(
+ context.getString(R.string.twitter_handel),
+ tweet.tweet.name
+ )
+ content.text = tweet.tweet.text
+
+ if (tweet.media.isNullOrEmpty()) {
+ mediaContainer.visibility = View.GONE
+ } else {
+ renderMedia(mediaContainer, tweet.media!!, onMediaClickListener)
+ }
+ } else {
+ // Removing '_normal' in profile image url because it's a low resolution image and
+ // will look blurry. There is no alternative solution for this and twitter
+ // recommends this. Url after removing `_normal` gives high resolution image.
+ Glide.with(context).load(
+ tweet.tweet.profileImageUrl.replace(
+ "_normal",
+ ""
+ )
+ ).transform(CircleTransform(context)).crossFade().into(quotedProfileImage)
+ quotedScreenName.text = tweet.tweet.screenName
+ quotedName.text = String.format(
+ context.getString(R.string.twitter_handel),
+ tweet.tweet.name
+ )
+ quotedContent.text = tweet.tweet.text
+ if (tweet.media.isNullOrEmpty()) {
+ quotedMediaContainer.visibility = View.GONE
+ } else {
+ renderMedia(quotedMediaContainer, tweet.media!!, onMediaClickListener)
+ }
+ quotedTweetScreenName.text = tweet.tweet.quotedTweet?.screenName
+ quotedTweetName.text = String.format(
+ context.getString(R.string.twitter_handel),
+ tweet.tweet.quotedTweet?.name
+ )
+ quotedTweetContent.text = tweet.tweet.quotedTweet?.text
+
+ if (tweet.quotedMedia.isNullOrEmpty()) {
+ quotedTweetMediaContainer.visibility = View.GONE
+ } else {
+ renderMedia(
+ quotedTweetMediaContainer, tweet.quotedMedia!!,
+ onMediaClickListener
+ )
+ }
+ }
+ }
+ }
+
+ private fun renderMedia(
+ mediaContainer: ViewGroup,
+ media: List,
+ onMediaClickListener: OnMediaClickListener
+ ) {
+ mediaContainer.visibility = View.VISIBLE
+ when (media.size) {
+ 1 -> {
+ when (media[0].type) {
+ Media.MEDIA_TYPE_PHOTO -> {
+ renderPhotos(mediaContainer, media)
+ }
+ Media.MEDIA_TYPE_GIF -> {
+ mediaContainer.run {
+ mediaOne.visibility = View.GONE
+ twoMediaContainer.visibility = View.GONE
+ threeMediaContainer.visibility = View.GONE
+ fourMediaContainer.visibility = View.GONE
+ videoContainer.visibility = View.GONE
+ gifContainer.visibility = View.VISIBLE
+ Glide.with(context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(gifImage)
+ gifContainer.setOnClickListener {
+ onMediaClickListener.onVideoOrGifClick(media[0].url)
+ }
+ }
+ }
+ Media.MEDIA_TYPE_VIDEO -> {
+ mediaContainer.run {
+ mediaOne.visibility = View.GONE
+ twoMediaContainer.visibility = View.GONE
+ threeMediaContainer.visibility = View.GONE
+ fourMediaContainer.visibility = View.GONE
+ videoContainer.visibility = View.VISIBLE
+ gifContainer.visibility = View.GONE
+ Glide.with(context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(videoImage)
+ videoContainer.setOnClickListener {
+ onMediaClickListener.onVideoOrGifClick(media[0].url)
+ }
+ }
+ }
+ }
+ }
+ else -> {
+ renderPhotos(mediaContainer, media)
+ }
+ }
+ }
+
+ private fun renderPhotos(mediaContainer: ViewGroup, media: List) {
+ mediaContainer.run {
+ mediaOne.visibility = if (media.size == 1) View.VISIBLE else View.GONE
+ twoMediaContainer.visibility = if (media.size == 2) View.VISIBLE else View.GONE
+ threeMediaContainer.visibility = if (media.size == 3) View.VISIBLE else View.GONE
+ fourMediaContainer.visibility = if (media.size == 4) View.VISIBLE else View.GONE
+ videoContainer.visibility = View.GONE
+ gifContainer.visibility = View.GONE
+ when (media.size) {
+ 1 -> {
+ Glide.with(context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(mediaOne)
+ }
+ 2 -> {
+ Glide.with(context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(twoImageMediaOne)
+ Glide.with(context).load("${media[1].mediaUrlHttps}:small")
+ .crossFade().into(twoImageMediaTwo)
+ }
+ 3 -> {
+ Glide.with(mediaContainer.context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(threeImageMediaOne)
+ Glide.with(mediaContainer.context).load("${media[1].mediaUrlHttps}:small")
+ .crossFade().into(threeImageMediaTwo)
+ Glide.with(mediaContainer.context).load("${media[2].mediaUrlHttps}:small")
+ .crossFade().into(threeImageMediaThree)
+ }
+ 4 -> {
+ Glide.with(mediaContainer.context).load("${media[0].mediaUrlHttps}:small")
+ .crossFade().into(fourImageMediaOne)
+ Glide.with(mediaContainer.context).load("${media[1].mediaUrlHttps}:small")
+ .crossFade().into(fourImageMediaTwo)
+ Glide.with(mediaContainer.context).load("${media[2].mediaUrlHttps}:small")
+ .crossFade().into(fourImageMediaThree)
+ Glide.with(mediaContainer.context).load("${media[3].mediaUrlHttps}:small")
+ .crossFade().into(fourImageMediaFour)
+ }
+ }
+ return@run
+ }
+ }
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterViewModel.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterViewModel.kt
new file mode 100644
index 0000000..4d7e0ff
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/social/TwitterViewModel.kt
@@ -0,0 +1,23 @@
+package com.mentalmachines.droidcon_boston.views.social
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.mentalmachines.droidcon_boston.domain.TwitterUseCase
+import com.mentalmachines.droidcon_boston.modal.Result
+import com.mentalmachines.droidcon_boston.modal.TweetWithMedia
+
+class TwitterViewModel(private val twitterUseCase: TwitterUseCase) : ViewModel() {
+
+ private val tweetLiveData: MutableLiveData>> by lazy {
+ MutableLiveData>>()
+ }
+
+ val tweets: LiveData>> by lazy {
+ twitterUseCase(Unit, tweetLiveData)
+ }
+
+ fun refreshTweets() {
+ twitterUseCase(Unit, tweetLiveData)
+ }
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerAdapterItem.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerAdapterItem.kt
index 46349ec..1f75dd1 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerAdapterItem.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerAdapterItem.kt
@@ -1,9 +1,9 @@
package com.mentalmachines.droidcon_boston.views.speaker
-import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.data.FirebaseDatabase.EventSpeaker
@@ -18,22 +18,22 @@ import eu.davidea.viewholders.FlexibleViewHolder
* Used for displaying speaker list items on the all speakers "Speakers" page.
*/
class SpeakerAdapterItem internal constructor(val itemData: EventSpeaker) :
- AbstractFlexibleItem() {
+ AbstractFlexibleItem() {
- override fun bindViewHolder(adapter: FlexibleAdapter>?,
- holder: ViewHolder, position: Int, payloads: MutableList?) {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>?,
+ holder: ViewHolder,
+ position: Int,
+ payloads: MutableList?
+ ) {
holder.name.text = itemData.name
holder.bio.text = itemData.bio.getHtmlFormattedSpanned()
val context = holder.name.context
- Glide.with(context)
- .load(itemData.pictureUrl)
- .transform(CircleTransform(context))
- .placeholder(R.drawable.emo_im_cool)
- .crossFade()
- .into(holder.avatar)
+ Glide.with(context).load(itemData.pictureUrl).transform(CircleTransform(context))
+ .placeholder(R.drawable.emo_im_cool).crossFade().into(holder.avatar)
}
override fun equals(other: Any?): Boolean {
@@ -52,7 +52,10 @@ class SpeakerAdapterItem internal constructor(val itemData: EventSpeaker) :
return R.layout.speaker_item
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return SpeakerAdapterItem.ViewHolder(view, adapter)
}
@@ -68,7 +71,11 @@ class SpeakerAdapterItem internal constructor(val itemData: EventSpeaker) :
findViews(view)
}
- constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(view, adapter, stickyHeader) {
+ constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(
+ view,
+ adapter,
+ stickyHeader
+ ) {
findViews(view)
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerFragment.kt
index 857c0ab..d7ab94f 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/speaker/SpeakerFragment.kt
@@ -2,12 +2,10 @@ package com.mentalmachines.droidcon_boston.views.speaker
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
@@ -19,6 +17,7 @@ import com.mentalmachines.droidcon_boston.views.detail.SpeakerDetailFragment
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import kotlinx.android.synthetic.main.speaker_fragment.*
+import timber.log.Timber
class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
@@ -26,8 +25,11 @@ class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
private val firebaseHelper = FirebaseHelper.instance
private lateinit var speakerAdapter: FlexibleAdapter
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.speaker_fragment, container, false)
}
@@ -43,7 +45,7 @@ class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
firebaseHelper.speakerDatabase.removeEventListener(dataListener)
}
- val dataListener: ValueEventListener = object : ValueEventListener {
+ private val dataListener: ValueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val rows = ArrayList()
for (speakerSnapshot in dataSnapshot.children) {
@@ -57,7 +59,7 @@ class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "detailQuery:onCancelled", databaseError.toException())
+ Timber.e(databaseError.toException())
}
}
@@ -73,16 +75,17 @@ class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
val arguments = Bundle()
- arguments.putString(EventSpeaker.SPEAKER_ITEM_ROW, gson.toJson(itemData, EventSpeaker::class.java))
+ arguments.putString(
+ EventSpeaker.SPEAKER_ITEM_ROW,
+ gson.toJson(itemData, EventSpeaker::class.java)
+ )
val speakerDetailFragment = SpeakerDetailFragment()
speakerDetailFragment.arguments = arguments
val fragmentManager = activity?.supportFragmentManager
- fragmentManager?.beginTransaction()
- ?.add(R.id.fragment_container, speakerDetailFragment)
- ?.addToBackStack(null)
- ?.commit()
+ fragmentManager?.beginTransaction()?.add(R.id.fragment_container, speakerDetailFragment)
+ ?.addToBackStack(null)?.commit()
}
return true
@@ -90,7 +93,8 @@ class SpeakerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
private fun setupSpeakerAdapter(rows: ArrayList) {
val items = rows.map { SpeakerAdapterItem(it) }
- speaker_recycler.layoutManager = LinearLayoutManager(speaker_recycler.context)
+ speaker_recycler.layoutManager =
+ androidx.recyclerview.widget.LinearLayoutManager(speaker_recycler.context)
speakerAdapter = FlexibleAdapter(items)
speakerAdapter.addListener(this)
speaker_recycler.adapter = speakerAdapter
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/transform/CircleTransform.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/transform/CircleTransform.kt
index e88ba9e..2d5954d 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/transform/CircleTransform.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/transform/CircleTransform.kt
@@ -12,7 +12,12 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
class CircleTransform(context: Context) : BitmapTransformation(context) {
- override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap? {
+ override fun transform(
+ pool: BitmapPool,
+ toTransform: Bitmap,
+ outWidth: Int,
+ outHeight: Int
+ ): Bitmap? {
return circleCrop(pool, toTransform)
}
@@ -43,4 +48,4 @@ class CircleTransform(context: Context) : BitmapTransformation(context) {
override fun getId(): String {
return javaClass.name
}
-}
\ No newline at end of file
+}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerAdapterItem.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerAdapterItem.kt
index f0ef738..5b9c222 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerAdapterItem.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerAdapterItem.kt
@@ -1,9 +1,9 @@
package com.mentalmachines.droidcon_boston.views.volunteer
-import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mentalmachines.droidcon_boston.R
import com.mentalmachines.droidcon_boston.data.FirebaseDatabase.VolunteerEvent
@@ -18,11 +18,14 @@ import eu.davidea.viewholders.FlexibleViewHolder
* Used for displaying volunteer list items on the all volunteers "volunteers" page.
*/
class VolunteerAdapterItem internal constructor(val itemData: VolunteerEvent) :
- AbstractFlexibleItem() {
-
- override fun bindViewHolder(adapter: FlexibleAdapter>?,
- holder: ViewHolder, position: Int, payloads: MutableList?) {
+ AbstractFlexibleItem() {
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>?,
+ holder: ViewHolder,
+ position: Int,
+ payloads: MutableList?
+ ) {
var bodyText = itemData.position
@@ -35,12 +38,8 @@ class VolunteerAdapterItem internal constructor(val itemData: VolunteerEvent) :
val context = holder.name.context
- Glide.with(context)
- .load(itemData.pictureUrl)
- .transform(CircleTransform(context))
- .placeholder(R.drawable.emo_im_cool)
- .crossFade()
- .into(holder.avatar)
+ Glide.with(context).load(itemData.pictureUrl).transform(CircleTransform(context))
+ .placeholder(R.drawable.emo_im_cool).crossFade().into(holder.avatar)
}
override fun equals(other: Any?): Boolean {
@@ -59,7 +58,10 @@ class VolunteerAdapterItem internal constructor(val itemData: VolunteerEvent) :
return R.layout.volunteer_item
}
- override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder {
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder {
return ViewHolder(view, adapter)
}
@@ -75,7 +77,11 @@ class VolunteerAdapterItem internal constructor(val itemData: VolunteerEvent) :
findViews(view)
}
- constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(view, adapter, stickyHeader) {
+ constructor(view: View, adapter: FlexibleAdapter<*>, stickyHeader: Boolean) : super(
+ view,
+ adapter,
+ stickyHeader
+ ) {
findViews(view)
}
diff --git a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerFragment.kt b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerFragment.kt
index d61ba6a..3c876fe 100644
--- a/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerFragment.kt
+++ b/Droidcon-Boston/app/src/main/java/com/mentalmachines/droidcon_boston/views/volunteer/VolunteerFragment.kt
@@ -3,12 +3,10 @@ package com.mentalmachines.droidcon_boston.views.volunteer
import android.content.Context
import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.fragment.app.Fragment
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
@@ -18,16 +16,19 @@ import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
import com.mentalmachines.droidcon_boston.utils.loadUriInCustomTab
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
-import kotlinx.android.synthetic.main.volunteer_fragment.volunteer_recycler
-
+import kotlinx.android.synthetic.main.volunteer_fragment.*
+import timber.log.Timber
class VolunteerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
private val firebaseHelper = FirebaseHelper.instance
private lateinit var volunteerAdapter: FlexibleAdapter
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.volunteer_fragment, container, false)
}
@@ -43,7 +44,7 @@ class VolunteerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
firebaseHelper.volunteerDatabase.removeEventListener(dataListener)
}
- val dataListener: ValueEventListener = object : ValueEventListener {
+ private val dataListener: ValueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val rows = ArrayList()
for (volunteerSnapshot in dataSnapshot.children) {
@@ -57,29 +58,36 @@ class VolunteerFragment : Fragment(), FlexibleAdapter.OnItemClickListener {
}
override fun onCancelled(databaseError: DatabaseError) {
- Log.e(javaClass.canonicalName, "detailQuery:onCancelled", databaseError.toException())
+ Timber.e(databaseError.toException())
}
}
private fun fetchDataFromFirebase() {
- firebaseHelper.volunteerDatabase.orderByChild("firstName").addValueEventListener(dataListener)
+ firebaseHelper.volunteerDatabase.orderByChild("firstName")
+ .addValueEventListener(dataListener)
}
override fun onItemClick(view: View, position: Int): Boolean {
val item = volunteerAdapter.getItem(position)
if (item is VolunteerAdapterItem && !item.itemData.twitter.isEmpty()) {
val context = activity as Context
- context.loadUriInCustomTab(String.format("%s%s", resources.getString(R.string.twitter_link), item.itemData.twitter))
+ context.loadUriInCustomTab(
+ String.format(
+ "%s%s",
+ resources.getString(R.string.twitter_link),
+ item.itemData.twitter
+ )
+ )
return false
}
return true // propagate.
}
-
private fun setupVolunteerAdapter(rows: ArrayList) {
val items = rows.map { VolunteerAdapterItem(it) }
- volunteer_recycler.layoutManager = LinearLayoutManager(volunteer_recycler.context)
+ volunteer_recycler.layoutManager =
+ androidx.recyclerview.widget.LinearLayoutManager(volunteer_recycler.context)
volunteerAdapter = FlexibleAdapter(items)
volunteerAdapter.addListener(this)
volunteer_recycler.adapter = volunteerAdapter
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.png
deleted file mode 100644
index 4691b33..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.webp b/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.webp
new file mode 100644
index 0000000..2b4c61a
Binary files /dev/null and b/Droidcon-Boston/app/src/main/res/drawable-nodpi/emo_im_cool.webp differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/logo_text.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/logo_text.png
deleted file mode 100644
index 91a2874..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/logo_text.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/navigation_header_image.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/navigation_header_image.png
deleted file mode 100644
index f24083a..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/navigation_header_image.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.png
deleted file mode 100644
index c647bea..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.webp b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.webp
new file mode 100644
index 0000000..cd0cb45
Binary files /dev/null and b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_facebook.webp differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.png
deleted file mode 100644
index 1302a0a..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.webp b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.webp
new file mode 100644
index 0000000..2732bdf
Binary files /dev/null and b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_instagram.webp differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.png
deleted file mode 100644
index 6f235b5..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.webp b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.webp
new file mode 100644
index 0000000..589c585
Binary files /dev/null and b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_linkedin.webp differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.png b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.png
deleted file mode 100644
index c23e58b..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.webp b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.webp
new file mode 100644
index 0000000..0df5166
Binary files /dev/null and b/Droidcon-Boston/app/src/main/res/drawable-nodpi/social_twitter.webp differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable-v24/ic_launcher_background.xml b/Droidcon-Boston/app/src/main/res/drawable-v24/ic_launcher_background.xml
new file mode 100644
index 0000000..644a23e
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable-v24/ic_launcher_background.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable-xxhdpi/droidcon_blank.png b/Droidcon-Boston/app/src/main/res/drawable-xxhdpi/droidcon_blank.png
deleted file mode 100644
index 715a439..0000000
Binary files a/Droidcon-Boston/app/src/main/res/drawable-xxhdpi/droidcon_blank.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/drawable/bg_quoted_tweet.xml b/Droidcon-Boston/app/src/main/res/drawable/bg_quoted_tweet.xml
new file mode 100644
index 0000000..3ff19b3
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/bg_quoted_tweet.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/drawable/bg_social_btn.xml b/Droidcon-Boston/app/src/main/res/drawable/bg_social_btn.xml
index 3006778..0a7a729 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/bg_social_btn.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/bg_social_btn.xml
@@ -1,8 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/drawable/circular_textview_accent_background.xml b/Droidcon-Boston/app/src/main/res/drawable/circular_textview_accent_background.xml
index 2bde016..80af40f 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/circular_textview_accent_background.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/circular_textview_accent_background.xml
@@ -1,6 +1,6 @@
+ android:shape="oval">
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_aboutus.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_aboutus.xml
index cc94088..0cbb996 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_aboutus.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_aboutus.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_agenda.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_agenda.xml
index 7322c92..b08dce9 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_agenda.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_agenda.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M17,10L7,10v2h10v-2zM19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,8h14v11zM14,14L7,14v2h7v-2z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_star.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml
similarity index 68%
rename from Droidcon-Boston/app/src/main/res/drawable/ic_star.xml
rename to Droidcon-Boston/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml
index a87ca09..beafea3 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_star.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24.0">
+ android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_close_black_24dp.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_close_black_24dp.xml
new file mode 100644
index 0000000..ede4b71
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_close_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_coc.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_coc.xml
index 99b5867..c5418b5 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_coc.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_coc.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_faq.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_faq.xml
index 4a3eb4f..1681cc0 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_faq.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_faq.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_gif.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_gif.xml
new file mode 100644
index 0000000..015c2b9
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_gif.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_heart.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_heart.xml
new file mode 100644
index 0000000..cfba5d8
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_heart.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_launcher_foreground.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..d560012
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_my_schedule.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_my_schedule.xml
index 811d5ac..b3b6e16 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_my_schedule.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_my_schedule.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M18,2H6c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM6,4h5v8l-2.5,-1.5L6,12V4z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_person_black_24dp.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_person_black_24dp.xml
new file mode 100644
index 0000000..b2cb337
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_person_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_play_circle_outline.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_play_circle_outline.xml
new file mode 100644
index 0000000..41bec11
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_play_circle_outline.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_search_white_24dp.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_search_white_24dp.xml
new file mode 100644
index 0000000..4d0f185
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_search_white_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_social.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_social.xml
index 49d69e0..564406c 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_social.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_social.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M20,12c0,-1.1 0.9,-2 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2v4c1.1,0 1.99,0.9 1.99,2s-0.89,2 -2,2v4c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2v-4c-1.1,0 -2,-0.9 -2,-2zM15.58,16.8L12,14.5l-3.58,2.3 1.08,-4.12 -3.29,-2.69 4.24,-0.25L12,5.8l1.54,3.95 4.24,0.25 -3.29,2.69 1.09,4.11z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_speakers.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_speakers.xml
index 4cfd869..603c006 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_speakers.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_speakers.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo.xml
new file mode 100644
index 0000000..6c65390
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo_blue.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo_blue.xml
new file mode 100644
index 0000000..1255644
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_twitter_logo_blue.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/ic_volunteers.xml b/Droidcon-Boston/app/src/main/res/drawable/ic_volunteers.xml
index 90ed54d..8a31e99 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/ic_volunteers.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/ic_volunteers.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ android:pathData="M19,2L5,2c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h4l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,5.3c1.49,0 2.7,1.21 2.7,2.7 0,1.49 -1.21,2.7 -2.7,2.7 -1.49,0 -2.7,-1.21 -2.7,-2.7 0,-1.49 1.21,-2.7 2.7,-2.7zM18,16L6,16v-0.9c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v0.9z" />
diff --git a/Droidcon-Boston/app/src/main/res/drawable/social_back.xml b/Droidcon-Boston/app/src/main/res/drawable/social_back.xml
index 1844a00..498f397 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/social_back.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/social_back.xml
@@ -1,5 +1,6 @@
-
+
-
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/drawable/social_press.xml b/Droidcon-Boston/app/src/main/res/drawable/social_press.xml
index 372f1f8..390e53a 100644
--- a/Droidcon-Boston/app/src/main/res/drawable/social_press.xml
+++ b/Droidcon-Boston/app/src/main/res/drawable/social_press.xml
@@ -1,5 +1,6 @@
-
+
-
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/drawable/splash_logo.xml b/Droidcon-Boston/app/src/main/res/drawable/splash_logo.xml
new file mode 100644
index 0000000..4981a10
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/splash_logo.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/drawable/splash_text.xml b/Droidcon-Boston/app/src/main/res/drawable/splash_text.xml
new file mode 100644
index 0000000..7717503
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/drawable/splash_text.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/layout/about_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/about_fragment.xml
index cae2c18..bbf6c1b 100644
--- a/Droidcon-Boston/app/src/main/res/layout/about_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/about_fragment.xml
@@ -13,30 +13,29 @@
android:id="@+id/imageView2"
android:layout_width="match_parent"
android:layout_height="200dp"
+ android:contentDescription="@string/navigation_header"
android:scaleType="centerCrop"
- android:src="@drawable/navigation_header_image"
- android:contentDescription="Navigation Header" />
+ android:src="@drawable/gradient_splash_background" />
+ android:src="@drawable/splash_text" />
+ tools:text="@tools:sample/lorem/random" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/agenda_day_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/agenda_day_fragment.xml
index 0f484b4..f729e59 100644
--- a/Droidcon-Boston/app/src/main/res/layout/agenda_day_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/agenda_day_fragment.xml
@@ -1,17 +1,44 @@
-
-
-
+
-
+ tools:listitem="@layout/schedule_item" />
-
+
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/layout/agenda_detail_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/agenda_detail_fragment.xml
index cc1fdf0..10b7012 100644
--- a/Droidcon-Boston/app/src/main/res/layout/agenda_detail_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/agenda_detail_fragment.xml
@@ -17,9 +17,9 @@
android:id="@+id/imgv_header_bg"
android:layout_width="match_parent"
android:layout_height="300dp"
- android:contentDescription="header Background"
+ android:contentDescription="@string/header_background"
android:scaleType="centerCrop"
- android:src="@drawable/navigation_header_image" />
+ android:src="@drawable/gradient_splash_background" />
+ tools:text="I'm a teacher and so are you --- lessons from teaching Android on the ground and in the cloud"
+ app:autoSizeMinTextSize="16sp"
+ app:autoSizeMaxTextSize="24sp"
+ app:autoSizeStepGranularity="1sp"
+ app:autoSizeTextType="uniform" />
-
+
+
+
+
+ app:srcCompat="@drawable/ic_heart" />
@@ -103,7 +120,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
- android:layout_marginRight="50dp"
tools:text="@tools:sample/full_names" />
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/layout/agenda_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/agenda_fragment.xml
index 4851290..42ddd2d 100644
--- a/Droidcon-Boston/app/src/main/res/layout/agenda_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/agenda_fragment.xml
@@ -5,16 +5,16 @@
android:layout_height="match_parent"
android:orientation="vertical">
-
+ app:tabTextAppearance="@style/AppTabText" />
-
+ android:textSize="17sp" />
diff --git a/Droidcon-Boston/app/src/main/res/layout/dialog_rating.xml b/Droidcon-Boston/app/src/main/res/layout/dialog_rating.xml
new file mode 100644
index 0000000..dca2fd3
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/dialog_rating.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/dialog_search.xml b/Droidcon-Boston/app/src/main/res/layout/dialog_search.xml
new file mode 100644
index 0000000..51cba7e
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/dialog_search.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/empty_filter_view.xml b/Droidcon-Boston/app/src/main/res/layout/empty_filter_view.xml
new file mode 100644
index 0000000..63c040c
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/empty_filter_view.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/empty_view.xml b/Droidcon-Boston/app/src/main/res/layout/empty_view.xml
index f22a227..a742f12 100644
--- a/Droidcon-Boston/app/src/main/res/layout/empty_view.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/empty_view.xml
@@ -1,8 +1,7 @@
-
+ android:textColor="?android:textColorSecondary" />
diff --git a/Droidcon-Boston/app/src/main/res/layout/faq_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/faq_fragment.xml
index 56c1b56..bbaaa12 100644
--- a/Droidcon-Boston/app/src/main/res/layout/faq_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/faq_fragment.xml
@@ -1,9 +1,8 @@
-
\ No newline at end of file
+ android:scrollbars="vertical" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/faq_header.xml b/Droidcon-Boston/app/src/main/res/layout/faq_header.xml
index 27fcd67..638894b 100644
--- a/Droidcon-Boston/app/src/main/res/layout/faq_header.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/faq_header.xml
@@ -1,14 +1,13 @@
-
-
-
+ android:textSize="17sp"
+ tools:text="Answer to your question" />
+ android:contentDescription="@string/hint_faq_photo"
+ tools:src="@drawable/splash_text" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/list_item_schedule_search.xml b/Droidcon-Boston/app/src/main/res/layout/list_item_schedule_search.xml
new file mode 100644
index 0000000..f1458ce
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/list_item_schedule_search.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/main_activity.xml b/Droidcon-Boston/app/src/main/res/layout/main_activity.xml
index 355dcc2..e8215e2 100644
--- a/Droidcon-Boston/app/src/main/res/layout/main_activity.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/main_activity.xml
@@ -1,5 +1,5 @@
-
-
-
-
-
+
-
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/nav_header.xml b/Droidcon-Boston/app/src/main/res/layout/nav_header.xml
index c44f77d..ac24b6e 100644
--- a/Droidcon-Boston/app/src/main/res/layout/nav_header.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/nav_header.xml
@@ -1,25 +1,28 @@
-
+ android:background="@color/colorPrimary"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark"
+ >
-
-
+ android:src="@drawable/splash_text"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintDimensionRatio="W,16:9"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.76" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/quoted_tweet_item.xml b/Droidcon-Boston/app/src/main/res/layout/quoted_tweet_item.xml
new file mode 100644
index 0000000..a3f0a62
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/quoted_tweet_item.xml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/schedule_item.xml b/Droidcon-Boston/app/src/main/res/layout/schedule_item.xml
index 2d6cf94..6592222 100644
--- a/Droidcon-Boston/app/src/main/res/layout/schedule_item.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/schedule_item.xml
@@ -1,12 +1,11 @@
-
+ android:foreground="?attr/selectableItemBackground">
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+ android:textSize="18sp"
+ tools:text="Title" />
+ android:textSize="16sp"
+ tools:text="10:00 am - 10:30 am" />
+ android:textSize="12sp" />
+ android:textSize="13sp"
+ tools:text="Room Blah Blah" />
+ tools:text="A Speaker, asdf asfafsafdasffas, asdf asfadsfasdf, asdf, asdfsadf, asdf, asdfasdfasf, asdf, asdf" />
@@ -117,16 +112,17 @@
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="center_vertical">
+
+ tools:src="@drawable/bg_social_btn" />
+
+ tools:visibility="visible" />
+ android:contentDescription="@string/hint_bookmark"
+ android:tint="@color/colorStar"
+ app:srcCompat="@drawable/ic_heart" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/schedule_item_header.xml b/Droidcon-Boston/app/src/main/res/layout/schedule_item_header.xml
index 4b123df..cd8a3ca 100644
--- a/Droidcon-Boston/app/src/main/res/layout/schedule_item_header.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/schedule_item_header.xml
@@ -1,6 +1,5 @@
-
+ android:background="@color/colorBackground" />
+
+ android:id="@+id/header_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:fontFamily="sans-serif"
+ android:maxLines="1"
+ android:paddingLeft="16dp"
+ android:paddingTop="2dp"
+ android:paddingRight="16dp"
+ android:paddingBottom="2dp"
+ android:textColor="@android:color/black"
+ android:textSize="14sp"
+ tools:text="10:30am" />
+
+ android:background="@color/colorBackground" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/social_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/social_fragment.xml
index 7b1b3c4..62c0610 100644
--- a/Droidcon-Boston/app/src/main/res/layout/social_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/social_fragment.xml
@@ -1,6 +1,6 @@
-
\ No newline at end of file
+ android:layout_height="match_parent"
+ android:background="@android:color/white" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/social_list_item.xml b/Droidcon-Boston/app/src/main/res/layout/social_list_item.xml
index d5050e0..96344ae 100644
--- a/Droidcon-Boston/app/src/main/res/layout/social_list_item.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/social_list_item.xml
@@ -10,8 +10,8 @@
android:id="@+id/social_item_img"
android:layout_width="48dp"
android:layout_height="48dp"
- tools:src="@drawable/social_facebook"
- android:contentDescription="Social Icon" />
+ android:contentDescription="@string/social_icon"
+ tools:src="@drawable/social_facebook" />
+ tools:text="Facebook" />
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/speaker_detail_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/speaker_detail_fragment.xml
index 65a237e..00b3e8c 100644
--- a/Droidcon-Boston/app/src/main/res/layout/speaker_detail_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/speaker_detail_fragment.xml
@@ -15,9 +15,9 @@
android:id="@+id/imgv_header_bg"
android:layout_width="match_parent"
android:layout_height="300dp"
- android:contentDescription="Header Background"
+ android:contentDescription="@string/header_background"
android:scaleType="centerCrop"
- android:src="@drawable/navigation_header_image" />
+ android:src="@drawable/gradient_splash_background" />
@@ -38,8 +38,8 @@
android:layout_below="@+id/imgv_speaker_detail_avatar"
android:layout_marginTop="@dimen/margin_normal"
android:gravity="center"
- android:paddingEnd="@dimen/def_padding"
android:paddingStart="@dimen/def_padding"
+ android:paddingEnd="@dimen/def_padding"
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold"
@@ -50,13 +50,12 @@
android:id="@+id/tv_speaker_detail_designation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentStart="true"
android:layout_below="@+id/tv_speaker_detail_name"
+ android:layout_alignParentStart="true"
android:fontFamily="sans-serif"
android:gravity="center"
- android:paddingEnd="@dimen/def_padding_large"
android:paddingStart="@dimen/def_padding_large"
+ android:paddingEnd="@dimen/def_padding_large"
android:textColor="@color/colorAccent"
android:textSize="18sp"
tools:text="Software Engineer @ Company" />
@@ -67,28 +66,29 @@
android:layout_height="wrap_content"
android:layout_above="@id/tv_speaker_detail_description"
android:layout_alignEnd="@id/imgv_header_bg"
- android:layout_alignRight="@id/imgv_header_bg"
android:layout_marginBottom="-20dp"
- android:paddingEnd="@dimen/def_padding"
- android:paddingLeft="@dimen/def_padding"
- android:paddingRight="@dimen/def_padding"
+ android:orientation="horizontal"
android:paddingStart="@dimen/def_padding"
- android:orientation="horizontal">
+ android:paddingLeft="@dimen/def_padding"
+ android:paddingEnd="@dimen/def_padding"
+ android:paddingRight="@dimen/def_padding">
@@ -101,9 +101,9 @@
android:layout_below="@id/imgv_header_bg"
android:layout_marginTop="25dp"
android:fontFamily="sans-serif"
- android:paddingBottom="@dimen/def_padding"
android:paddingLeft="@dimen/def_padding_large"
android:paddingRight="@dimen/def_padding_large"
+ android:paddingBottom="@dimen/def_padding"
android:textColor="@android:color/black"
tools:text="@tools:sample/lorem/random" />
diff --git a/Droidcon-Boston/app/src/main/res/layout/speaker_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/speaker_fragment.xml
index 3965659..1c54c3b 100644
--- a/Droidcon-Boston/app/src/main/res/layout/speaker_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/speaker_fragment.xml
@@ -1,5 +1,5 @@
-
-
+ tools:visibility="visible" />
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
-
+ android:foregroundGravity="center"
+ android:maxWidth="600dp"
+ android:src="@drawable/splash_text"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/tweet_item_layout.xml b/Droidcon-Boston/app/src/main/res/layout/tweet_item_layout.xml
new file mode 100644
index 0000000..704fc34
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/tweet_item_layout.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_error_view.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_error_view.xml
new file mode 100644
index 0000000..7756048
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_error_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_four_image_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_four_image_layout.xml
new file mode 100644
index 0000000..9567131
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_four_image_layout.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_fragment.xml
new file mode 100644
index 0000000..c4010de
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_fragment.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_gif_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_gif_layout.xml
new file mode 100644
index 0000000..f3ec479
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_gif_layout.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_one_image_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_one_image_layout.xml
new file mode 100644
index 0000000..4831a83
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_one_image_layout.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_three_image_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_three_image_layout.xml
new file mode 100644
index 0000000..59802a6
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_three_image_layout.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_two_image_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_two_image_layout.xml
new file mode 100644
index 0000000..9fcae21
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_two_image_layout.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/twitter_video_layout.xml b/Droidcon-Boston/app/src/main/res/layout/twitter_video_layout.xml
new file mode 100644
index 0000000..f5274b7
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/layout/twitter_video_layout.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/layout/volunteer_fragment.xml b/Droidcon-Boston/app/src/main/res/layout/volunteer_fragment.xml
index 21b67bf..73fc8fb 100644
--- a/Droidcon-Boston/app/src/main/res/layout/volunteer_fragment.xml
+++ b/Droidcon-Boston/app/src/main/res/layout/volunteer_fragment.xml
@@ -1,5 +1,5 @@
-
-
+ tools:visibility="visible" />
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/menu/nav_menu.xml b/Droidcon-Boston/app/src/main/res/menu/nav_menu.xml
index 1be5fb1..875be92 100644
--- a/Droidcon-Boston/app/src/main/res/menu/nav_menu.xml
+++ b/Droidcon-Boston/app/src/main/res/menu/nav_menu.xml
@@ -14,10 +14,6 @@
android:icon="@drawable/ic_faq"
android:title="@string/str_faq" />
-
+
+
+
+
+
+
+
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 4ae7d12..bbd3e02 100644
--- a/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 4ae7d12..bbd3e02 100644
--- a/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/Droidcon-Boston/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher.png
index d187a8c..05ec996 100755
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
deleted file mode 100644
index a32b496..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index 82501b6..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index cafd17a..bad0451 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/Droidcon-Boston/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher.png
index b4b4ed2..89021bd 100755
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
deleted file mode 100644
index 38bec58..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index c191c6b..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 9ffa995..b1e30c7 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/Droidcon-Boston/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 78f6b3f..4d17ebf 100755
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
deleted file mode 100644
index af26f02..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index ed950ae..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index a4139ee..41512f0 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/Droidcon-Boston/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 113a28e..e2eac8e 100755
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
deleted file mode 100644
index 7212f53..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 5bed947..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index cd45fb4..2e94ee7 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/Droidcon-Boston/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index da7d480..7cf855f 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
deleted file mode 100644
index 1955f7f..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 5db1835..0000000
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 3980212..c4bf59f 100644
Binary files a/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/Droidcon-Boston/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/Droidcon-Boston/app/src/main/res/values-v21/styles.xml b/Droidcon-Boston/app/src/main/res/values-v21/styles.xml
deleted file mode 100644
index f8ef6e6..0000000
--- a/Droidcon-Boston/app/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/values/colors.xml b/Droidcon-Boston/app/src/main/res/values/colors.xml
index 7b654bb..cb49912 100644
--- a/Droidcon-Boston/app/src/main/res/values/colors.xml
+++ b/Droidcon-Boston/app/src/main/res/values/colors.xml
@@ -1,12 +1,13 @@
- #090909
- #000000
- #B1EF36
+ #8548a3
+ #543179
+ #6ec24b
+
#1100FAAE
#FFEEEEEE
#9e9e9e
#e0e0e0
#484848
- #eb8a2d
+ @color/colorAccent
diff --git a/Droidcon-Boston/app/src/main/res/values/dimens.xml b/Droidcon-Boston/app/src/main/res/values/dimens.xml
index a20d895..81ed3d6 100644
--- a/Droidcon-Boston/app/src/main/res/values/dimens.xml
+++ b/Droidcon-Boston/app/src/main/res/values/dimens.xml
@@ -17,4 +17,10 @@
80dp
65dp
+
+
+ 20dp
+ 16dp
+ 20dp
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/values/ic_launcher_background.xml b/Droidcon-Boston/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..263efad
--- /dev/null
+++ b/Droidcon-Boston/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #8548A3
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/main/res/values/strings.xml b/Droidcon-Boston/app/src/main/res/values/strings.xml
index 229a055..2177970 100644
--- a/Droidcon-Boston/app/src/main/res/values/strings.xml
+++ b/Droidcon-Boston/app/src/main/res/values/strings.xml
@@ -26,6 +26,10 @@
Speakers
Volunteers
Code Of Conduct
+ Log In
+ Log Out
+ Twitter Feed
+ Error
Facebook
Instagram
LinkedIn
@@ -43,4 +47,31 @@
Close
droidconbos
No talks added to My Schedule\nfor this day
+ Sorry we couldn\'t fetch tweets :(\nSwipe down to
+ retry.
+ Navigation Header
+ Logo
+ Header Background
+ Navigation Header Image
+ Social Icon
+ LinkedIn Icon
+ Twitter Icon
+ Droidcon Boston Android logo
+ Jump to Current
+ Search
+ Speaker, Talk, Topic…
+ Close Search
+ Clear Search
+ There are no talks that match this search.
+ What did you think?
+ Rate
+ Submit
+ Cancel
+ Please select a rating.
+ Share this session
+
+ \@%s
+
+ NpgDY5dYI3M2is9V4mbK0IKMS
+ JhaeIidtw4MnY9tvbsxIFSn2CUxn9B4lLYc3WVcu6eZmDc9HAo
diff --git a/Droidcon-Boston/app/src/main/res/values/styles.xml b/Droidcon-Boston/app/src/main/res/values/styles.xml
index 6ebbf17..5a7520a 100644
--- a/Droidcon-Boston/app/src/main/res/values/styles.xml
+++ b/Droidcon-Boston/app/src/main/res/values/styles.xml
@@ -12,12 +12,15 @@
@@ -35,14 +38,31 @@
- 20sp
+
+
+
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/EventbriteTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/EventbriteTest.kt
new file mode 100644
index 0000000..1cbcdde
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/EventbriteTest.kt
@@ -0,0 +1,59 @@
+package com.mentalmachines.droidcon_boston.data
+
+import com.google.gson.Gson
+import com.mentalmachines.droidcon_boston.utils.ServiceLocator
+import junit.framework.TestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.FileInputStream
+import java.io.InputStreamReader
+
+@RunWith(JUnit4::class)
+class EventbriteTest : TestCase() {
+
+ private val gson: Gson = ServiceLocator.gson
+
+ private var eventbriteUser: EventbriteUser? = null
+
+ @Before
+ fun setupTests() {
+ eventbriteUser = gson.fromJson(
+ InputStreamReader(FileInputStream("sampledata/sample_eb_result.json")),
+ EventbriteUser::class.java
+ )
+ }
+
+ @Test
+ fun testUserInitialized() {
+ assertNotNull(eventbriteUser)
+ }
+
+ @Test
+ fun testUserHasTwitter() {
+ var handle: String? = ""
+ eventbriteUser?.let { user ->
+ for (question: EventbriteQuestion in user.answers) {
+ if (question.question_id == "20940481") {
+ handle = question.answer
+ }
+ }
+ }
+ assertNotNull(handle)
+ assertEquals("@ccorrads", handle)
+ }
+
+ @Test
+ fun testQuestionType() {
+ var type: QuestionType? = null
+ eventbriteUser?.let { user ->
+ for (question: EventbriteQuestion in user.answers) {
+ if (question.question_id == "20940481") {
+ type = question.type
+ }
+ }
+ }
+ assertEquals(QuestionType.Text, type)
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabaseTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabaseTest.kt
new file mode 100644
index 0000000..de8c7d3
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/FirebaseDatabaseTest.kt
@@ -0,0 +1,179 @@
+package com.mentalmachines.droidcon_boston.data
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.threeten.bp.ZoneOffset.UTC
+import org.threeten.bp.ZonedDateTime
+import org.threeten.bp.format.DateTimeFormatter
+
+class FirebaseDatabaseTest {
+
+ private val firebaseDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.S'Z'")
+
+ private val notifTime = 10L
+ private val primarySpeakerName = "Primary Speaker"
+ private val startTime = "2018-03-26T15:00:00.000Z"
+ private val startTimeFormatted = "11:00 am"
+ private val dateFormatted = "03/26/2018"
+ private val talkTitle = "Talk Name"
+ private val speakerOrg = "Speaker Org"
+ private val speakerPicture = "Picture URL"
+ private val speakerNames = hashMapOf(primarySpeakerName to true, "Second Speaker" to true)
+ private val speakerNamesToPhotos =
+ hashMapOf(primarySpeakerName to speakerPicture, "Second" to "Second Photo")
+ private val speakerNameToOrg =
+ hashMapOf(primarySpeakerName to speakerOrg, "Second Speaker" to "Second Company")
+ private val roomNames = hashMapOf("Talk Room" to true)
+ private val speakerIds = hashMapOf("SpeakerOne" to true, "SpeakerTwo" to true)
+ private val roomIds = hashMapOf("RoomOne" to true)
+ private val description = "Talk Description"
+ private val photo = hashMapOf("Photo" to "Photo")
+ private val endTime = "2018-03-26T15:45:00.000Z"
+ private val endTimeFormatted = "11:45 am"
+ private val trackSortOrder = 0
+ private val scheduleId = "ScheduleId"
+ private val speakerBio = "Speaker Bio"
+ private val facebookProfile = "Facebook Profile"
+ private val linkedinProfile = "LinkedIn Profile"
+ private val twitterProfile = "Twitter Profile"
+ private val socialProfiles = hashMapOf(
+ "facebook" to facebookProfile, "linkedIn" to
+ linkedinProfile,
+ "twitter" to twitterProfile
+ )
+
+ private fun createDummyScheduleEvent(): FirebaseDatabase.ScheduleEvent {
+ return FirebaseDatabase.ScheduleEvent(
+ notifTime,
+ primarySpeakerName,
+ startTime,
+ talkTitle,
+ speakerNames,
+ speakerNamesToPhotos,
+ speakerNameToOrg,
+ roomNames,
+ speakerIds,
+ roomIds,
+ description,
+ photo,
+ endTime,
+ trackSortOrder
+ )
+ }
+
+ @Test
+ fun scheduleEventToScheduleRow() {
+
+ val testEvent = createDummyScheduleEvent()
+
+ val testRow = testEvent.toScheduleRow(scheduleId)
+
+ assertEquals(primarySpeakerName, testRow.primarySpeakerName)
+ assertEquals(scheduleId, testRow.id)
+ assertEquals(startTimeFormatted, testRow.startTime)
+ assertEquals(talkTitle, testRow.talkTitle)
+ assertEquals(speakerNames.count(), testRow.speakerCount)
+ assertEquals(description, testRow.talkDescription)
+ assertEquals(speakerNames.keys.toList(), testRow.speakerNames)
+ assertEquals(speakerNameToOrg, testRow.speakerNameToOrgName)
+ assertEquals(startTime, testRow.utcStartTimeString)
+ assertEquals(endTimeFormatted, testRow.endTime)
+ assertEquals(roomNames.keys.first(), testRow.room)
+ assertEquals(dateFormatted, testRow.date)
+ assertEquals(trackSortOrder, testRow.trackSortOrder)
+ assertEquals(speakerNamesToPhotos, testRow.photoUrlMap)
+ assertTrue(testRow.isOver)
+ }
+
+ @Test
+ fun scheduleEventToScheduleRow_isCurrentSession_true_whenNowIsBetweenStartAndEndTime() {
+
+ val now = ZonedDateTime.now(UTC)
+ val startTime = now.minusMinutes(30)
+ val endTime = now.plusMinutes(30)
+
+ val testEvent = createDummyScheduleEvent().copy(
+ startTime = startTime.format(firebaseDateFormatter),
+ endTime = endTime.format(firebaseDateFormatter)
+ )
+
+ val testRow = testEvent.toScheduleRow(scheduleId)
+
+ assertTrue(testRow.isCurrentSession)
+ }
+
+
+ @Test
+ fun scheduleEventToScheduleRow_isCurrentSession_false_whenNowIsBeforeStartTime() {
+
+ val now = ZonedDateTime.now(UTC)
+ val startTime = now.plusMinutes(30)
+ val endTime = now.plusMinutes(90)
+
+ val testEvent = createDummyScheduleEvent().copy(
+ startTime = startTime.format(firebaseDateFormatter),
+ endTime = endTime.format(firebaseDateFormatter)
+ )
+
+ val testRow = testEvent.toScheduleRow(scheduleId)
+
+ assertFalse(testRow.isCurrentSession)
+ }
+
+ @Test
+ fun scheduleEventToScheduleRow_isCurrentSession_false_whenNowIsAfterEndTime() {
+
+ val now = ZonedDateTime.now(UTC)
+ val startTime = now.minusMinutes(90)
+ val endTime = now.minusMinutes(30)
+
+ val testEvent = createDummyScheduleEvent().copy(
+ startTime = startTime.format(firebaseDateFormatter),
+ endTime = endTime.format(firebaseDateFormatter)
+ )
+
+ val testRow = testEvent.toScheduleRow(scheduleId)
+
+ assertFalse(testRow.isCurrentSession)
+ }
+
+ @Test
+ fun scheduleEventToScheduleRow_isCurrentSession_true_ifItBeginsInLessThan15Minutes() {
+
+ // Set startTime to 14 minutes from now in order to simulate the scenario where now is in between sessions.
+ val now = ZonedDateTime.now(UTC)
+ val startTime = now.plusMinutes(TIME_BETWEEN_SESSIONS - 1)
+ val endTime = now.plusMinutes(60)
+
+ val testEvent = createDummyScheduleEvent().copy(
+ startTime = startTime.format(firebaseDateFormatter),
+ endTime = endTime.format(firebaseDateFormatter)
+ )
+
+ val testRow = testEvent.toScheduleRow(scheduleId)
+
+ assertTrue(testRow.isCurrentSession)
+ }
+
+ @Test
+ fun eventSpeakerToScheduleDetail() {
+ val testEventSpeaker = FirebaseDatabase.EventSpeaker(
+ speakerPicture,
+ socialProfiles,
+ speakerBio,
+ talkTitle,
+ speakerOrg,
+ primarySpeakerName
+ )
+
+ val scheduleRow = Schedule.ScheduleRow()
+ val scheduleDetail = testEventSpeaker.toScheduleDetail(scheduleRow)
+ assertEquals(scheduleRow.id, scheduleDetail.id)
+ assertEquals(facebookProfile, scheduleDetail.facebook)
+ assertEquals(linkedinProfile, scheduleDetail.linkedIn)
+ assertEquals(twitterProfile, scheduleDetail.twitter)
+ assertEquals(speakerBio, scheduleDetail.speakerBio)
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/ScheduleTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/ScheduleTest.kt
index 6e1ecc3..d787ec2 100644
--- a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/ScheduleTest.kt
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/ScheduleTest.kt
@@ -1,9 +1,11 @@
package com.mentalmachines.droidcon_boston.data
import com.mentalmachines.droidcon_boston.data.Schedule.ScheduleRow
-import org.junit.*
-import org.junit.Assert.*
-import java.util.Arrays
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.util.*
class ScheduleTest {
@@ -45,12 +47,61 @@ class ScheduleTest {
}
@Test
- fun getSpeakerStringMultipleSpeakers() {
+ fun getSpeakerStringNoSpeaker() {
+ val row = ScheduleRow()
+ assertEquals("", row.getSpeakerString())
+ }
+
+ @Test
+ fun getSpeakerStringOneSpeaker() {
val row = ScheduleRow()
+ row.speakerNames = Arrays.asList(s1)
+ assertEquals(s1, row.getSpeakerString())
+ }
+ @Test
+ fun getSpeakerStringMultipleSpeakers() {
+ val row = ScheduleRow()
row.speakerNames = Arrays.asList(s1, s2)
- val speakerNameString = row.getSpeakerString()!!
- assertTrue(speakerNameString.contains(s1))
- assertTrue(speakerNameString.contains(s2))
+ assertEquals("$s1, $s2", row.getSpeakerString())
+ }
+
+ @Test
+ fun containsKeywordInTitle() {
+ val keyword = "Kotlin"
+ val testTitle = "All About $keyword"
+
+ val row = ScheduleRow(talkTitle = testTitle)
+ assertTrue(row.containsKeyword(keyword))
+ }
+
+ @Test
+ fun containsKeywordInDescription() {
+ val keyword = "Kotlin"
+ val testDescription = "All about $keyword"
+
+ val row = ScheduleRow(talkDescription = testDescription)
+ assertTrue(row.containsKeyword(keyword))
+ }
+
+ @Test
+ fun containsKeywordInSpeakerName() {
+ val keyword = "Kotlin"
+ val speakers = listOf("Speaker about $keyword")
+
+ val row = ScheduleRow(speakerNames = speakers)
+ assertTrue(row.containsKeyword(keyword))
+ }
+
+ @Test
+ fun containsEmptyKeyword() {
+ val row = ScheduleRow()
+ assertTrue(row.containsKeyword(""))
+ }
+
+ @Test
+ fun doesNotContainKeyword() {
+ val row = ScheduleRow()
+ assertFalse(row.containsKeyword("Blah"))
}
}
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepoTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepoTest.kt
new file mode 100644
index 0000000..d8c9068
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserAgendaRepoTest.kt
@@ -0,0 +1,40 @@
+package com.mentalmachines.droidcon_boston.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+
+class UserAgendaRepoTest {
+ private val mockSharedPrefs = mock(SharedPreferences::class.java)
+ private val mockEditor = mock(SharedPreferences.Editor::class.java)
+ private val mockContext = mock(Context::class.java)
+ private lateinit var userAgendaRepo: UserAgendaRepo
+
+ @Before
+ fun setup() {
+ `when`(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPrefs)
+ `when`(mockSharedPrefs.edit()).thenReturn(mockEditor)
+ `when`(mockEditor.putStringSet(anyString(), any())).thenReturn(mockEditor)
+ userAgendaRepo = UserAgendaRepo.getInstance(mockContext)
+ }
+
+ @Test
+ fun testBookmarkAndRemove() {
+ val sessionId = "Session ID"
+ assertFalse(userAgendaRepo.isSessionBookmarked(sessionId))
+
+ userAgendaRepo.bookmarkSession(sessionId, true)
+ assertTrue(userAgendaRepo.isSessionBookmarked(sessionId))
+
+ userAgendaRepo.bookmarkSession(sessionId, false)
+ assertFalse(userAgendaRepo.isSessionBookmarked(sessionId))
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserProfileTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserProfileTest.kt
new file mode 100644
index 0000000..2382f69
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/data/UserProfileTest.kt
@@ -0,0 +1,43 @@
+package com.mentalmachines.droidcon_boston.data
+
+import com.google.firebase.auth.FirebaseUser
+import com.mentalmachines.droidcon_boston.firebase.AuthController
+import junit.framework.TestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import java.util.UUID
+import kotlin.collections.HashSet
+
+@RunWith(JUnit4::class)
+class UserProfileTest : TestCase() {
+
+ private var localUser: FirebaseDatabase.User? = null
+
+ @Mock
+ private lateinit var firebaseUser: FirebaseUser
+
+ @Before
+ fun setUpTests() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun testProfileCreation() {
+ val id = UUID.randomUUID().toString()
+ Mockito.doReturn("joe@smith.com").`when`(firebaseUser).email
+ Mockito.doReturn(id).`when`(firebaseUser).uid
+ Mockito.doReturn("Joe Smith").`when`(firebaseUser).displayName
+
+ localUser = AuthController.createLocalUser(HashSet(), firebaseUser)
+
+ assertNotNull(localUser)
+ assertEquals("Joe Smith", localUser?.displayName)
+ assertEquals("joe@smith.com", localUser?.username)
+ assertEquals(id, localUser?.id)
+ }
+}
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelperRobot.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelperRobot.kt
new file mode 100644
index 0000000..8ed7615
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/firebase/FirebaseHelperRobot.kt
@@ -0,0 +1,46 @@
+package com.mentalmachines.droidcon_boston.firebase
+
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseReference
+import com.google.firebase.database.ValueEventListener
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+
+/**
+ * A robot class that can be used to extract out firebase functionality from the tests.
+ *
+ * Mainly we can use this to extract mocking.
+ */
+class FirebaseHelperRobot(mockFirebaseHelper: FirebaseHelper) {
+ private val mockSpeakerReference = mock(DatabaseReference::class.java)
+
+ init {
+ `when`(mockFirebaseHelper.speakerDatabase).thenReturn(mockSpeakerReference)
+ `when`(mockSpeakerReference.orderByChild(anyString())).thenReturn(mockSpeakerReference)
+ }
+
+ fun mockSpeakers(speakers: List): FirebaseHelperRobot {
+ val speakerSnapshots = speakers.map { speaker ->
+ val mockSnapshot = mock(DataSnapshot::class.java)
+ `when`(mockSnapshot.getValue(FirebaseDatabase.EventSpeaker::class.java)).thenReturn(
+ speaker
+ )
+ return@map mockSnapshot
+ }
+
+ val mockSnapshot = mock(DataSnapshot::class.java)
+ `when`(mockSnapshot.children).thenReturn(speakerSnapshots)
+
+ doAnswer {
+ val valueEventListener = it.arguments.first() as ValueEventListener
+ valueEventListener.onDataChange(mockSnapshot)
+ return@doAnswer null
+ }.`when`(mockSpeakerReference).addValueEventListener(any(ValueEventListener::class.java))
+
+ return this
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModelTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModelTest.kt
new file mode 100644
index 0000000..6e1805f
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/detail/AgendaDetailViewModelTest.kt
@@ -0,0 +1,172 @@
+package com.mentalmachines.droidcon_boston.views.detail
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.mentalmachines.droidcon_boston.R
+import com.mentalmachines.droidcon_boston.data.FirebaseDatabase
+import com.mentalmachines.droidcon_boston.data.Schedule
+import com.mentalmachines.droidcon_boston.data.UserAgendaRepo
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelper
+import com.mentalmachines.droidcon_boston.firebase.FirebaseHelperRobot
+import com.mentalmachines.droidcon_boston.views.rating.RatingRepo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+
+class AgendaDetailViewModelTest {
+ private val mockFirebaseHelper = mock(FirebaseHelper::class.java)
+ private val talkTitle = "Talk Title"
+ private val talkRoom = "Talk Room"
+ private val startTime = "2018-03-26T15:00:00.000Z"
+ private val endTime = "2018-03-26T15:45:00.000Z"
+ private val scheduleId = "Schedule ID"
+ private val primarySpeakerName = "John"
+ private val secondarySpeakerName = "Bob"
+ private val speakerOrg = "Speaker Org"
+ private val speakerPhoto = "Speaker Photo"
+ private val speakerNames = listOf(primarySpeakerName, secondarySpeakerName)
+
+ private val scheduleRow = Schedule.ScheduleRow(
+ talkTitle = talkTitle,
+ room = talkRoom,
+ startTime = startTime,
+ endTime = endTime,
+ id = scheduleId,
+ speakerNames = speakerNames,
+ primarySpeakerName = primarySpeakerName,
+ photoUrlMap = hashMapOf(primarySpeakerName to speakerPhoto),
+ speakerNameToOrgName = hashMapOf(primarySpeakerName to speakerOrg)
+ )
+
+ private lateinit var mockAgendaRepo: UserAgendaRepo
+ private lateinit var mockRatingRepo: RatingRepo
+ private lateinit var viewModel: AgendaDetailViewModel
+
+ @JvmField
+ @Rule
+ val instantTaskExecutor = InstantTaskExecutorRule()
+
+ @Before
+ fun setup() {
+ val mockContext = mock(Context::class.java)
+ val mockPreferences = mock(SharedPreferences::class.java)
+ val mockEditor = mock(SharedPreferences.Editor::class.java)
+
+ `when`(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockPreferences)
+ `when`(mockPreferences.edit()).thenReturn(mockEditor)
+ `when`(mockEditor.putStringSet(anyString(), any())).thenReturn(mockEditor)
+
+ mockAgendaRepo = UserAgendaRepo.getInstance(mockContext)
+ mockRatingRepo = mock(RatingRepo::class.java)
+ viewModel = AgendaDetailViewModel(scheduleRow, mockAgendaRepo, mockRatingRepo,
+ mockFirebaseHelper)
+ }
+
+
+ @Test
+ fun getTalkTitle() {
+ assertEquals(talkTitle, viewModel.talkTitle)
+ }
+
+ @Test
+ fun getRoom() {
+ assertEquals(talkRoom, viewModel.room)
+ }
+
+ @Test
+ fun getStartTime() {
+ assertEquals(startTime, viewModel.startTime)
+ }
+
+ @Test
+ fun getEndTime() {
+ assertEquals(endTime, viewModel.endTime)
+ }
+
+ @Test
+ fun getScheduleRowId() {
+ assertEquals(scheduleId, viewModel.schedulerowId)
+ }
+
+ @Test
+ fun getSpeakerNames() {
+ assertEquals(speakerNames, viewModel.speakerNames)
+ }
+
+ fun isBookmarked() {
+ val eventSpeaker = FirebaseDatabase.EventSpeaker(name = primarySpeakerName)
+ FirebaseHelperRobot(mockFirebaseHelper).mockSpeakers(listOf(eventSpeaker))
+
+ mockAgendaRepo.bookmarkSession(scheduleId, true)
+
+ viewModel.loadData()
+ assertTrue(viewModel.isBookmarked)
+ assertEquals(R.color.colorAccent, viewModel.bookmarkColorRes)
+ }
+
+ fun isNotBookmarked() {
+ val eventSpeaker = FirebaseDatabase.EventSpeaker(name = primarySpeakerName)
+ FirebaseHelperRobot(mockFirebaseHelper).mockSpeakers(listOf(eventSpeaker))
+
+ mockAgendaRepo.bookmarkSession(scheduleId, false)
+
+ viewModel.loadData()
+ assertFalse(viewModel.isBookmarked)
+ assertEquals(R.color.colorLightGray, viewModel.bookmarkColorRes)
+ }
+
+ fun loadData() {
+ val eventSpeaker = FirebaseDatabase.EventSpeaker(name = primarySpeakerName)
+ FirebaseHelperRobot(mockFirebaseHelper).mockSpeakers(listOf(eventSpeaker))
+
+ viewModel.loadData()
+
+ val scheduleDetail = eventSpeaker.toScheduleDetail(scheduleRow)
+ assertEquals(scheduleDetail, viewModel.scheduleDetail.value)
+ }
+
+ fun getSpeakerWithSpeaker() {
+ val eventSpeaker = FirebaseDatabase.EventSpeaker(name = primarySpeakerName)
+ FirebaseHelperRobot(mockFirebaseHelper).mockSpeakers(listOf(eventSpeaker))
+
+ viewModel.loadData()
+ assertEquals(eventSpeaker, viewModel.getSpeaker(primarySpeakerName))
+ }
+
+ @Test
+ fun getSpeakerNoSpeaker() {
+ assertNull(viewModel.getSpeaker("blahblahblah"))
+ }
+
+ @Test
+ fun getOrganizationForSpeaker() {
+ assertEquals(speakerOrg, viewModel.getOrganizationForSpeaker(primarySpeakerName))
+ }
+
+ @Test
+ fun getPhotoForSpeaker() {
+ assertEquals(speakerPhoto, viewModel.getPhotoForSpeaker(primarySpeakerName))
+ }
+
+ fun toggleBookmark() {
+ val eventSpeaker = FirebaseDatabase.EventSpeaker(name = primarySpeakerName)
+ FirebaseHelperRobot(mockFirebaseHelper).mockSpeakers(listOf(eventSpeaker))
+
+ viewModel.loadData()
+ assertFalse(viewModel.isBookmarked)
+
+ viewModel.toggleBookmark()
+
+ assertTrue(viewModel.isBookmarked)
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModelTest.kt b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModelTest.kt
new file mode 100644
index 0000000..ee0e5d1
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/views/rating/RatingViewModelTest.kt
@@ -0,0 +1,26 @@
+package com.mentalmachines.droidcon_boston.views.rating
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+class RatingViewModelTest {
+ private val ratingRepo = mock(RatingRepo::class.java)
+ private val viewModel = RatingViewModel().init("myUserID", ratingRepo)
+
+ @JvmField
+ @Rule
+ val instantTaskExecutor = InstantTaskExecutorRule()
+
+ @Test
+ fun handleSubmission() {
+ val rating = 5
+ val feedback = "Loved it!"
+ val sessionId = "sessionId"
+
+ viewModel.handleSubmission(rating, feedback, sessionId)
+ assertTrue(viewModel.getFeedbackSent().value == true)
+ }
+}
\ No newline at end of file
diff --git a/Droidcon-Boston/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/Droidcon-Boston/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..ca6ee9c
--- /dev/null
+++ b/Droidcon-Boston/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/Droidcon-Boston/build.gradle b/Droidcon-Boston/build.gradle
index 46c1b05..4990472 100644
--- a/Droidcon-Boston/build.gradle
+++ b/Droidcon-Boston/build.gradle
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.2.30'
+ ext.kotlin_version = '1.3.21'
repositories {
google()
jcenter()
@@ -8,16 +8,25 @@ buildscript {
maven {
url 'https://maven.fabric.io/public'
}
+
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.0.1'
+ classpath 'com.android.tools.build:gradle:3.3.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
- classpath 'com.google.gms:google-services:3.2.0'
+ classpath 'com.google.gms:google-services:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'io.fabric.tools:gradle:1.25.1'
+ classpath 'io.fabric.tools:gradle:1.28.0'
+
+ // static analysis plugins
+ classpath 'com.novoda:gradle-static-analysis-plugin:0.8'
+ classpath 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.0-RC12'
+ classpath 'gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:5.1.0'
}
}
diff --git a/Droidcon-Boston/gradle.properties b/Droidcon-Boston/gradle.properties
index 332bd55..9351488 100644
--- a/Droidcon-Boston/gradle.properties
+++ b/Droidcon-Boston/gradle.properties
@@ -15,4 +15,6 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-BASE_API_URL="http://www.droidcon-boston.com/wp-json/wp/v2/"
\ No newline at end of file
+BASE_API_URL="http://www.droidcon-boston.com/wp-json/wp/v2/"
+android.useAndroidX=true
+android.enableJetifier=true
\ No newline at end of file
diff --git a/Droidcon-Boston/gradle/wrapper/gradle-wrapper.properties b/Droidcon-Boston/gradle/wrapper/gradle-wrapper.properties
index 52b6e6b..2c508e6 100644
--- a/Droidcon-Boston/gradle/wrapper/gradle-wrapper.properties
+++ b/Droidcon-Boston/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
diff --git a/Droidcon-Boston/team-props/detekt-config.yml b/Droidcon-Boston/team-props/detekt-config.yml
new file mode 100644
index 0000000..26ddbed
--- /dev/null
+++ b/Droidcon-Boston/team-props/detekt-config.yml
@@ -0,0 +1,345 @@
+autoCorrect: true
+
+test-pattern:
+ active: true
+ patterns:
+ - '.*/test/.*'
+ - '.*Test.kt'
+ - '.*Spec.kt'
+ exclude-rule-sets:
+ - 'comments'
+ exclude-rules:
+ - 'NamingRules'
+ - 'WildcardImport'
+ - 'MagicNumber'
+ - 'MaxLineLength'
+ - 'LateinitUsage'
+ - 'StringLiteralDuplication'
+ - 'SpreadOperator'
+ - 'TooManyFunctions'
+
+processors:
+ active: true
+
+console-reports:
+ active: true
+
+output-reports:
+ active: true
+
+comments:
+ active: true
+ CommentOverPrivateFunction:
+ active: false
+ CommentOverPrivateProperty:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$)
+ UndocumentedPublicClass:
+ active: false
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ UndocumentedPublicFunction:
+ active: false
+
+complexity:
+ active: true
+ ComplexCondition:
+ active: true
+ ComplexInterface:
+ active: false
+ includeStaticDeclarations: false
+ ComplexMethod:
+ active: true
+ ignoreSingleWhenExpression: false
+ LabeledExpression:
+ active: false
+ LargeClass:
+ active: true
+ LongMethod:
+ active: true
+ LongParameterList:
+ active: true
+ ignoreDefaultParameters: false
+ MethodOverloading:
+ active: false
+ NestedBlockDepth:
+ active: true
+ StringLiteralDuplication:
+ active: false
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: true
+ thresholdInFiles: 11
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: "^(ignore|expected).*"
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: false
+ methodNames: 'toString,hashCode,equals,finalize'
+ InstanceOfCheckForException:
+ active: false
+ NotImplementedDeclaration:
+ active: false
+ PrintStackTrace:
+ active: false
+ RethrowCaughtException:
+ active: false
+ ReturnFromFinally:
+ active: false
+ SwallowedException:
+ active: false
+ ThrowingExceptionFromFinally:
+ active: false
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: false
+ exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
+ ThrowingNewInstanceOfSameException:
+ active: false
+ TooGenericExceptionCaught:
+ active: true
+ exceptionNames:
+ - ArrayIndexOutOfBoundsException
+ - Error
+ - Exception
+ - IllegalMonitorStateException
+ - NullPointerException
+ - IndexOutOfBoundsException
+ - RuntimeException
+ - Throwable
+ TooGenericExceptionThrown:
+ active: true
+ exceptionNames:
+ - Error
+ - Exception
+ - Throwable
+ - RuntimeException
+
+naming:
+ active: true
+ ClassNaming:
+ active: true
+ classPattern: '[A-Z$][a-zA-Z0-9$]*'
+ EnumNaming:
+ active: true
+ enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ forbiddenName: ''
+ FunctionMaxLength:
+ active: false
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: false
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
+ excludeClassPattern: '$^'
+ MatchingDeclarationName:
+ active: true
+ MemberNameEqualsClassName:
+ active: false
+ ignoreOverriddenFunction: true
+ ObjectPropertyNaming:
+ active: true
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
+ TopLevelPropertyNaming:
+ active: true
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[a-z][A-Za-z\d]*'
+ privatePropertyPattern: '(_)?[a-z][A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ForEachOnRange:
+ active: true
+ SpreadOperator:
+ active: true
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ DuplicateCaseInWhenExpression:
+ active: true
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: false
+ EqualsWithHashCodeExist:
+ active: true
+ ExplicitGarbageCollectionCall:
+ active: true
+ InvalidRange:
+ active: false
+ IteratorHasNextCallsNextMethod:
+ active: false
+ IteratorNotThrowingNoSuchElementException:
+ active: false
+ LateinitUsage:
+ active: false
+ excludeAnnotatedProperties: ""
+ ignoreOnClassesPattern: ""
+ UnconditionalJumpStatementInLoop:
+ active: false
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: false
+ UnsafeCast:
+ active: false
+ UselessPostfixExpression:
+ active: false
+ WrongEqualsTypeParameter:
+ active: false
+
+style:
+ active: true
+ CollapsibleIfStatements:
+ active: false
+ DataClassContainsFunctions:
+ active: false
+ conversionFunctionPrefix: 'to'
+ EqualsNullCall:
+ active: false
+ ExpressionBodySyntax:
+ active: false
+ ForbiddenComment:
+ active: true
+ values: 'TODO:,FIXME:,STOPSHIP:'
+ ForbiddenImport:
+ active: false
+ imports: ''
+ FunctionOnlyReturningConstant:
+ active: false
+ ignoreOverridableFunction: true
+ excludedFunctions: 'describeContents'
+ LoopWithTooManyJumpStatements:
+ active: false
+ maxJumpCount: 1
+ MagicNumber:
+ active: true
+ ignoreNumbers: '-1,0,1,2'
+ ignoreHashCodeFunction: false
+ ignorePropertyDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ MaxLineLength:
+ active: true
+ maxLineLength: 120
+ excludePackageStatements: false
+ excludeImportStatements: false
+ MayBeConst:
+ active: false
+ ModifierOrder:
+ active: true
+ NestedClassesVisibility:
+ active: false
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: false
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: false
+ OptionalWhenBraces:
+ active: false
+ ProtectedMemberInFinalClass:
+ active: false
+ RedundantVisibilityModifierRule:
+ active: false
+ ReturnCount:
+ active: true
+ max: 2
+ excludedFunctions: "equals"
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: false
+ SpacingBetweenPackageAndImports:
+ active: false
+ ThrowsCount:
+ active: true
+ max: 2
+ TrailingWhitespace:
+ active: false
+ UnnecessaryAbstractClass:
+ active: false
+ UnnecessaryInheritance:
+ active: false
+ UnnecessaryParentheses:
+ active: false
+ UntilInsteadOfRangeTo:
+ active: false
+ UnusedImports:
+ active: false
+ UnusedPrivateMember:
+ active: false
+ UseDataClass:
+ active: false
+ excludeAnnotatedClasses: ""
+ UtilityClassWithPublicConstructor:
+ active: false
+ WildcardImport:
+ active: true
+ excludeImports: 'java.util.*,kotlinx.android.synthetic.*'
\ No newline at end of file
diff --git a/Droidcon-Boston/team-props/lint-config.xml b/Droidcon-Boston/team-props/lint-config.xml
new file mode 100644
index 0000000..026cc87
--- /dev/null
+++ b/Droidcon-Boston/team-props/lint-config.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 955bb57..c7d1d95 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Droidcon Boston - Conference Android App
-Official conference android app for Droidcon Boston.
+Official conference Android app for Droidcon Boston.
-
+
[License: Apache License Version 2.0](LICENSE.txt)
diff --git a/img/readme_img.png b/img/readme_img.png
new file mode 100644
index 0000000..f7937c0
Binary files /dev/null and b/img/readme_img.png differ
diff --git a/img/splash_screen_bg.png b/img/splash_screen_bg.png
deleted file mode 100644
index d7c7a9b..0000000
Binary files a/img/splash_screen_bg.png and /dev/null differ