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 @@ - - - - - - \ 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 @@ + + + + \ 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 @@ + + + + + + + + + + + + +