diff --git a/.circleci/config.yml b/.circleci/config.yml index 6594d52b1..bcef43d29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,5 @@ cache_version_keys: &cache_version_keys - CACHE_VERSION_OF_PROJECT_DEPS: v1 + CACHE_VERSION_OF_PROJECT_DEPS: v2 CACHE_VERSION_OF_DANGER_CACHE: v1 cache_keys: @@ -8,13 +8,11 @@ cache_keys: keys: &all_keys_of_gradle_cache - *primary_key_of_gradle_cache - gradle-cache-{{ checksum "~/CACHE_VERSION_OF_PROJECT_DEPS" }}- - - gradle-cache- danger_cache: primary: &primary_key_of_danger_cache danger-cache-{{ checksum "~/CACHE_VERSION_OF_DANGER_CACHE" }}-{{ checksum "~/danger_cache" }} keys: &all_keys_of_danger_cache - *primary_key_of_danger_cache - danger-cache-{{ checksum "~/CACHE_VERSION_OF_DANGER_CACHE" }}- - - danger-cache- docker_env: android_defaults: &android_defaults @@ -54,20 +52,19 @@ jobs: - run: *init_bash - restore_cache: &restore_gradle_cache keys: *all_keys_of_gradle_cache - - run: + - run: &download_all_dependencies name: Download Dependencies - command: ./gradlew androidDependencies + command: retry_command ./gradlew androidDependenciesExtra getDependencies - save_cache: &save_gradle_cache paths: - ~/.android - ~/.gradle - .gradle - - ~/.m2 key: *primary_key_of_gradle_cache - run: name: Assemble apk command: | - ./gradlew assembleDebug # --offline # build with online-mode for now + ./gradlew clean assembleDebug --offline - store_artifacts: path: frontend/android/build/outputs/apk - run: *download_dpg @@ -108,7 +105,9 @@ jobs: - checkout - run: *init_bash - restore_cache: *restore_gradle_cache - - run: ./gradlew testDebugUnitTest lintDebug ktlint --continue + - run: *download_all_dependencies + - save_cache: *save_gradle_cache + - run: ./gradlew testDebugUnitTest lintDebug ktlint --continue --offline - run: name: Merge junit report files command: | diff --git a/.circleci/scripts/retry_command b/.circleci/scripts/retry_command new file mode 100755 index 000000000..e9dd154f6 --- /dev/null +++ b/.circleci/scripts/retry_command @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -eu + +: "${MAX_RETRY_COUNT:=3}" + +retry() { + local retry=0 + + while let "$MAX_RETRY_COUNT > $retry"; do + let "retry=$retry+1" + + "$@" && exit 0 + + sleep 3 + done + + echo "Failed to process : $@" 1>&2 + exit 1 +} + +retry "$@" \ No newline at end of file diff --git a/.idea/dictionaries/dic.xml b/.idea/dictionaries/dic.xml index dc0229ef3..194022d1d 100644 --- a/.idea/dictionaries/dic.xml +++ b/.idea/dictionaries/dic.xml @@ -3,6 +3,7 @@ circleci confsched + coroutine databinding deploygate droidkaigi @@ -16,4 +17,4 @@ stetho - + \ No newline at end of file diff --git a/README.md b/README.md index ef94684c0..ea0b59ab6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ You can download the binary built on master branch from [Try it on your device via DeployGate](https://dply.me/t6sc7f#install) +NOTE: Google Play Protect will show a warning dialog on some of devices when installing the current apk. The detailed specification of Google Play Protect is not public so we cannot address this matter. Please ignore the dialog for now. If you cannot install this apk without any error message, please disable Google Play Protect from Google Play Store's menus. Sorry for the inconvenience. # Features @@ -18,7 +19,7 @@ You can download the binary built on master branch from [Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/api/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/api/response/SessionResponse.kt b/data/api/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/api/response/SessionResponse.kt index 549125023..d7387d6cc 100644 --- a/data/api/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/api/response/SessionResponse.kt +++ b/data/api/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/api/response/SessionResponse.kt @@ -15,5 +15,7 @@ interface SessionResponse { val message: SessionMessageResponse? val isPlenumSession: Boolean val sessionType: String? + val videoUrl: String? + val slideUrl: String? val interpretationTarget: Boolean } diff --git a/data/db-room/build.gradle b/data/db-room/build.gradle index 673230ddb..ab6c0b9b8 100644 --- a/data/db-room/build.gradle +++ b/data/db-room/build.gradle @@ -14,7 +14,7 @@ dependencies { api project(":ext:android-extension") implementation Dep.Kotlin.stdlibJvm - implementation Dep.Kotlin.serialization + implementation Dep.Kotlin.serializationCommon api Dep.Kotlin.coroutines api Dep.AndroidX.Room.runtime api Dep.AndroidX.Room.coroutine diff --git a/data/db-room/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/data/db-room/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java deleted file mode 100644 index 461cd5732..000000000 --- a/data/db-room/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertEquals; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/CacheDatabase.kt b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/CacheDatabase.kt index 3b309bbfd..251ae9439 100644 --- a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/CacheDatabase.kt +++ b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/CacheDatabase.kt @@ -22,7 +22,7 @@ import io.github.droidkaigi.confsched2019.data.db.entity.SponsorEntityImpl (SponsorEntityImpl::class), (SessionFeedbackImpl::class) ], - version = 8 + version = 9 ) abstract class CacheDatabase : RoomDatabase() { abstract fun sessionDao(): SessionDao diff --git a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntityImpl.kt b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntityImpl.kt index 25139ea31..8cdd53536 100644 --- a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntityImpl.kt +++ b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntityImpl.kt @@ -16,6 +16,8 @@ data class SessionEntityImpl( override var sessionFormat: String?, override val sessionType: String?, override val intendedAudience: String?, + override val videoUrl: String?, + override val slideUrl: String?, override val isInterpretationTarget: Boolean, @Embedded override var language: LanguageEntityImpl?, @Embedded override val category: CategoryEntityImpl?, diff --git a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/mapper/SessionDataMapperExt.kt b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/mapper/SessionDataMapperExt.kt index 9738d0fb8..376a92173 100644 --- a/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/mapper/SessionDataMapperExt.kt +++ b/data/db-room/src/main/java/io/github/droidkaigi/confsched2019/data/db/entity/mapper/SessionDataMapperExt.kt @@ -77,6 +77,8 @@ fun SessionResponse.toSessionEntityImpl( requireNotNull(category.translatedName?.en) ), intendedAudience = intendedAudience, + videoUrl = videoUrl, + slideUrl = slideUrl, isInterpretationTarget = interpretationTarget, room = RoomEntityImpl(roomId, rooms.roomName(roomId)), sessionType = sessionType @@ -95,6 +97,8 @@ fun SessionResponse.toSessionEntityImpl( category = null, room = RoomEntityImpl(roomId, rooms.roomName(roomId)), intendedAudience = null, + videoUrl = videoUrl, + slideUrl = slideUrl, isInterpretationTarget = interpretationTarget, message = message?.let { MessageEntityImpl(requireNotNull(it.ja), requireNotNull(it.en)) diff --git a/data/db/build.gradle b/data/db/build.gradle index 4eabcd6b3..7e60b98cc 100644 --- a/data/db/build.gradle +++ b/data/db/build.gradle @@ -8,6 +8,13 @@ apply from: rootProject.file('gradle/android.gradle') kotlin { targets { fromPreset(presets.android, 'android') + + final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \ + ? presets.iosArm64 : presets.iosX64 + + fromPreset(iOSTarget, 'iOS') { + compilations.main.outputKinds('FRAMEWORK') + } } sourceSets { commonMain.dependencies { diff --git a/data/db/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java b/data/db/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/data/db/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/db/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntity.kt b/data/db/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntity.kt index c8b50364e..fad5335fa 100644 --- a/data/db/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntity.kt +++ b/data/db/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/db/entity/SessionEntity.kt @@ -11,6 +11,8 @@ interface SessionEntity { val language: LanguageEntity? val category: CategoryEntity? val intendedAudience: String? + val videoUrl: String? + val slideUrl: String? val isInterpretationTarget: Boolean val room: RoomEntity? val message: MessageEntity? diff --git a/data/firestore-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/data/firestore-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/data/firestore-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FireStoreComponent.kt b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreComponent.kt similarity index 69% rename from data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FireStoreComponent.kt rename to data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreComponent.kt index 78f9958fd..55ebaccaa 100644 --- a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FireStoreComponent.kt +++ b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreComponent.kt @@ -2,7 +2,7 @@ package io.github.droidkaigi.confsched2019.data.repository import dagger.BindsInstance import dagger.Component -import io.github.droidkaigi.confsched2019.data.firestore.FireStore +import io.github.droidkaigi.confsched2019.data.firestore.Firestore import io.github.droidkaigi.confsched2019.data.firestore.FirestoreModule import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -13,18 +13,18 @@ import kotlin.coroutines.CoroutineContext FirestoreModule::class ] ) -interface FireStoreComponent { - fun fireStore(): FireStore +interface FirestoreComponent { + fun firestore(): Firestore @Component.Builder interface Builder { @BindsInstance fun coroutineContext(coroutineContext: CoroutineContext): Builder - fun build(): FireStoreComponent + fun build(): FirestoreComponent } companion object { - fun builder(): Builder = DaggerFireStoreComponent.builder() + fun builder(): Builder = DaggerFirestoreComponent.builder() } } diff --git a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreImpl.kt b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreImpl.kt index b139b7ca1..17a6163ba 100644 --- a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreImpl.kt +++ b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreImpl.kt @@ -13,9 +13,9 @@ import io.github.droidkaigi.confsched2019.model.Announcement import kotlinx.coroutines.tasks.await import javax.inject.Inject -class FirestoreImpl @Inject constructor() : FireStore { +class FirestoreImpl @Inject constructor() : Firestore { - override suspend fun getFavoriteSessionIds(): List { + override suspend fun getFavoriteSessionIds(): List { if (FirebaseAuth.getInstance().currentUser?.uid == null) return listOf() val favoritesRef = getFavoritesRef() val snapshot = favoritesRef @@ -25,7 +25,7 @@ class FirestoreImpl @Inject constructor() : FireStore { } val favorites = favoritesRef.whereEqualTo("favorite", true).fastGet() - return favorites.documents.mapNotNull { it.id.toIntOrNull() } + return favorites.documents.mapNotNull { it.id } } override suspend fun toggleFavorite(sessionId: String) { diff --git a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreModule.kt b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreModule.kt index d3829fd76..3c6d27be9 100644 --- a/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreModule.kt +++ b/data/firestore-impl/src/main/java/io/github/droidkaigi/confsched2019/data/firestore/FirestoreModule.kt @@ -5,7 +5,7 @@ import dagger.Module @Module(includes = [FirestoreModule.Providers::class]) internal abstract class FirestoreModule { - @Binds abstract fun fireStore(impl: FirestoreImpl): FireStore + @Binds abstract fun firestore(impl: FirestoreImpl): Firestore @Module internal object Providers diff --git a/data/firestore/build.gradle b/data/firestore/build.gradle index 6856c1f99..efd94ad76 100644 --- a/data/firestore/build.gradle +++ b/data/firestore/build.gradle @@ -8,6 +8,13 @@ apply from: rootProject.file('gradle/android.gradle') kotlin { targets { fromPreset(presets.android, 'android') + + final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \ + ? presets.iosArm64 : presets.iosX64 + + fromPreset(iOSTarget, 'iOS') { + compilations.main.outputKinds('FRAMEWORK') + } } sourceSets { commonMain.dependencies { diff --git a/data/firestore/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java b/data/firestore/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/data/firestore/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/firestore/FireStore.kt b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/firestore/FireStore.kt index 90becd5ce..62d1cd2fb 100644 --- a/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/firestore/FireStore.kt +++ b/data/firestore/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/firestore/FireStore.kt @@ -2,8 +2,8 @@ package io.github.droidkaigi.confsched2019.data.firestore import io.github.droidkaigi.confsched2019.model.Announcement -interface FireStore { - suspend fun getFavoriteSessionIds(): List +interface Firestore { + suspend fun getFavoriteSessionIds(): List suspend fun toggleFavorite(sessionId: String) suspend fun getAnnouncements(): List } diff --git a/data/repository-impl/build.gradle b/data/repository-impl/build.gradle index f62c40096..ee5e37047 100644 --- a/data/repository-impl/build.gradle +++ b/data/repository-impl/build.gradle @@ -11,11 +11,10 @@ dependencies { implementation project(":data:api") implementation project(":data:db") implementation project(":data:firestore") - implementation project(":ext:log") api project(":model") implementation Dep.Kotlin.stdlibJvm - implementation Dep.Kotlin.serialization + implementation Dep.Kotlin.serializationCommon api Dep.Kotlin.coroutines implementation Dep.AndroidX.lifecycleLiveData kapt Dep.AndroidX.Room.compiler diff --git a/data/repository-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/data/repository-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/data/repository-impl/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/DataSessionRepository.kt b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/DataSessionRepository.kt index c85c9389c..bd666e579 100644 --- a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/DataSessionRepository.kt +++ b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/DataSessionRepository.kt @@ -4,7 +4,7 @@ import com.soywiz.klock.DateTime import io.github.droidkaigi.confsched2019.data.api.DroidKaigiApi import io.github.droidkaigi.confsched2019.data.api.GoogleFormApi import io.github.droidkaigi.confsched2019.data.db.SessionDatabase -import io.github.droidkaigi.confsched2019.data.firestore.FireStore +import io.github.droidkaigi.confsched2019.data.firestore.Firestore import io.github.droidkaigi.confsched2019.data.repository.mapper.toSession import io.github.droidkaigi.confsched2019.model.Lang import io.github.droidkaigi.confsched2019.model.Session @@ -19,7 +19,7 @@ class DataSessionRepository @Inject constructor( private val droidKaigiApi: DroidKaigiApi, private val googleFormApi: GoogleFormApi, private val sessionDatabase: SessionDatabase, - private val fireStore: FireStore + private val firestore: Firestore ) : SessionRepository { override suspend fun sessionContents(): SessionContents = coroutineScope { @@ -39,7 +39,7 @@ class DataSessionRepository @Inject constructor( val sessionsAsync = async { sessionDatabase.sessions() } val allSpeakersAsync = async { sessionDatabase.allSpeaker() } val fabSessionIdsAsync = async { - fireStore.getFavoriteSessionIds() + firestore.getFavoriteSessionIds() } val sessionEntities = sessionsAsync.await() @@ -55,8 +55,8 @@ class DataSessionRepository @Inject constructor( )) } - override suspend fun toggleFavorite(session: Session.SpeechSession) { - fireStore.toggleFavorite(session.id) + override suspend fun toggleFavorite(session: Session) { + firestore.toggleFavorite(session.id) } override suspend fun submitSessionFeedback( diff --git a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/RepositoryComponent.kt b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/RepositoryComponent.kt index 253050ea1..bb8d541f7 100644 --- a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/RepositoryComponent.kt +++ b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/RepositoryComponent.kt @@ -6,7 +6,7 @@ import io.github.droidkaigi.confsched2019.data.api.DroidKaigiApi import io.github.droidkaigi.confsched2019.data.api.GoogleFormApi import io.github.droidkaigi.confsched2019.data.db.SessionDatabase import io.github.droidkaigi.confsched2019.data.db.SponsorDatabase -import io.github.droidkaigi.confsched2019.data.firestore.FireStore +import io.github.droidkaigi.confsched2019.data.firestore.Firestore import javax.inject.Singleton @Singleton @@ -28,7 +28,7 @@ interface RepositoryComponent { @BindsInstance fun database(database: SessionDatabase): Builder @BindsInstance fun sponsorDatabase(database: SponsorDatabase): Builder - @BindsInstance fun fireStore(fireStore: FireStore): Builder + @BindsInstance fun firestore(firestore: Firestore): Builder fun build(): RepositoryComponent } diff --git a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/mapper/SessionMappers.kt b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/mapper/SessionMappers.kt index 18fef898e..cb4081f11 100644 --- a/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/mapper/SessionMappers.kt +++ b/data/repository-impl/src/main/java/io/github/droidkaigi/confsched2019/data/repository/mapper/SessionMappers.kt @@ -4,16 +4,15 @@ import com.soywiz.klock.DateTime import io.github.droidkaigi.confsched2019.data.db.entity.SessionWithSpeakers import io.github.droidkaigi.confsched2019.data.db.entity.SpeakerEntity import io.github.droidkaigi.confsched2019.model.Category -import io.github.droidkaigi.confsched2019.model.LocaledString import io.github.droidkaigi.confsched2019.model.Room import io.github.droidkaigi.confsched2019.model.Session -import io.github.droidkaigi.confsched2019.model.SessionMessage import io.github.droidkaigi.confsched2019.model.SessionType import io.github.droidkaigi.confsched2019.model.Speaker +import io.github.droidkaigi.confsched2019.model.LocaledString fun SessionWithSpeakers.toSession( speakerEntities: List, - favList: List?, + favList: List?, firstDay: DateTime ): Session { return if (session.isServiceSession) { @@ -28,7 +27,8 @@ fun SessionWithSpeakers.toSession( room = requireNotNull(session.room).let { room -> Room(room.id, room.name) }, - sessionType = SessionType.of(session.sessionType) + sessionType = SessionType.of(session.sessionType), + isFavorited = favList!!.contains(session.id) ) } else { require(speakerIdList.isNotEmpty()) @@ -64,11 +64,13 @@ fun SessionWithSpeakers.toSession( ) }, intendedAudience = session.intendedAudience, + videoUrl = session.videoUrl, + slideUrl = session.slideUrl, isInterpretationTarget = session.isInterpretationTarget, - isFavorited = favList!!.map { it.toString() }.contains(session.id), + isFavorited = favList!!.contains(session.id), speakers = speakers, message = session.message?.let { - SessionMessage(it.ja, it.en) + LocaledString(it.ja, it.en) } ) } diff --git a/data/repository/build.gradle b/data/repository/build.gradle index 6856c1f99..efd94ad76 100644 --- a/data/repository/build.gradle +++ b/data/repository/build.gradle @@ -8,6 +8,13 @@ apply from: rootProject.file('gradle/android.gradle') kotlin { targets { fromPreset(presets.android, 'android') + + final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \ + ? presets.iosArm64 : presets.iosX64 + + fromPreset(iOSTarget, 'iOS') { + compilations.main.outputKinds('FRAMEWORK') + } } sourceSets { commonMain.dependencies { diff --git a/data/repository/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java b/data/repository/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/data/repository/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/data/repository/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/repository/SessionRepository.kt b/data/repository/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/repository/SessionRepository.kt index e42ef532d..bd320259d 100644 --- a/data/repository/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/repository/SessionRepository.kt +++ b/data/repository/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/data/repository/SessionRepository.kt @@ -7,7 +7,7 @@ import io.github.droidkaigi.confsched2019.model.SessionFeedback interface SessionRepository { suspend fun sessionContents(): SessionContents suspend fun refresh() - suspend fun toggleFavorite(session: Session.SpeechSession) + suspend fun toggleFavorite(session: Session) suspend fun submitSessionFeedback( session: Session.SpeechSession, sessionFeedback: SessionFeedback diff --git a/ext/android-extension/src/androidTest/java/io/github/droidkaigi/confsched2019/ext/android/ExampleInstrumentedTest.java b/ext/android-extension/src/androidTest/java/io/github/droidkaigi/confsched2019/ext/android/ExampleInstrumentedTest.java deleted file mode 100644 index 5346f9796..000000000 --- a/ext/android-extension/src/androidTest/java/io/github/droidkaigi/confsched2019/ext/android/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.ext.android; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ext.android.test", appContext.getPackageName()); - } -} diff --git a/ext/log/build.gradle b/ext/log/build.gradle index 8470119cf..e3f4d5940 100644 --- a/ext/log/build.gradle +++ b/ext/log/build.gradle @@ -8,14 +8,23 @@ apply from: rootProject.file('gradle/android.gradle') kotlin { targets { fromPreset(presets.android, 'android') + + final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \ + ? presets.iosArm64 : presets.iosX64 + + fromPreset(iOSTarget, 'iOS') { + compilations.main.outputKinds('FRAMEWORK') + } } sourceSets { commonMain.dependencies { api Dep.Kotlin.stdlibCommon + api Dep.Timber.common } androidMain { dependencies { api Dep.Kotlin.stdlibJvm + api Dep.Timber.jdk } } commonTest.dependencies { diff --git a/ext/log/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java b/ext/log/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java deleted file mode 100644 index e367b66e7..000000000 --- a/ext/log/src/androidTest/java/io/github/droidkaigi/confsched2019/ui/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); - } -} diff --git a/ext/log/src/commonMain/kotlin/Logger.kt b/ext/log/src/commonMain/kotlin/Logger.kt deleted file mode 100644 index eb4839dca..000000000 --- a/ext/log/src/commonMain/kotlin/Logger.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.util - -enum class LogLevel { - DEBUG, WARN, ERROR -} - -var logHandler = { logLevel: LogLevel, tag: String, e: Throwable?, messageHandler: () -> String -> } - -fun logd( - tag: String = "droidkaigi", - e: Throwable? = null, - messageHandler: () -> String = { "" } -) = logHandler(LogLevel.DEBUG, tag, e, messageHandler) - -fun logw( - tag: String = "droidkaigi", - e: Throwable? = null, - messageHandler: () -> String = { "" } -) = logHandler(LogLevel.WARN, tag, e, messageHandler) - -fun loge( - tag: String = "droidkaigi", - e: Throwable? = null, - messageHandler: () -> String = { "" } -) = logHandler(LogLevel.ERROR, tag, e, messageHandler) diff --git a/ext/log/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/timber/Timber.kt b/ext/log/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/timber/Timber.kt new file mode 100644 index 000000000..8ea4f05dd --- /dev/null +++ b/ext/log/src/commonMain/kotlin/io/github/droidkaigi/confsched2019/timber/Timber.kt @@ -0,0 +1,29 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package io.github.droidkaigi.confsched2019.timber + +import timber.log.Timber + +inline fun Timber.assert(throwable: Throwable) { + Timber.log(ASSERT, null, throwable, null) +} + +inline fun Timber.error(throwable: Throwable) { + Timber.log(ERROR, null, throwable, null) +} + +inline fun Timber.warn(throwable: Throwable) { + Timber.log(WARNING, null, throwable, null) +} + +inline fun Timber.info(throwable: Throwable) { + Timber.log(INFO, null, throwable, null) +} + +inline fun Timber.debug(throwable: Throwable) { + Timber.log(DEBUG, null, throwable, null) +} + +inline fun Timber.verbose(throwable: Throwable) { + Timber.log(VERBOSE, null, throwable, null) +} diff --git a/feature/about/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/about/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..497b0644c 100644 --- a/feature/about/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/about/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.about.test", appContext.getPackageName()); } } diff --git a/feature/about/src/main/java/io/github/droidkaigi/confsched2019/about/ui/item/AboutSection.kt b/feature/about/src/main/java/io/github/droidkaigi/confsched2019/about/ui/item/AboutSection.kt index 30cb5d184..fe2e666bc 100644 --- a/feature/about/src/main/java/io/github/droidkaigi/confsched2019/about/ui/item/AboutSection.kt +++ b/feature/about/src/main/java/io/github/droidkaigi/confsched2019/about/ui/item/AboutSection.kt @@ -34,7 +34,7 @@ class AboutSection @Inject constructor( R.string.about_privacy_policy, R.string.about_check ) { - Toast.makeText(it, "FIXME!!", Toast.LENGTH_SHORT).show() + activityActionCreator.openUrl("http://www.association.droidkaigi.jp/privacy") }, AboutItem( R.string.about_license, diff --git a/feature/about/src/main/res/drawable/ic_github_black_24dp.xml b/feature/about/src/main/res/drawable/ic_github_black_36dp.xml similarity index 95% rename from feature/about/src/main/res/drawable/ic_github_black_24dp.xml rename to feature/about/src/main/res/drawable/ic_github_black_36dp.xml index 505243e1f..30fed3ee6 100644 --- a/feature/about/src/main/res/drawable/ic_github_black_24dp.xml +++ b/feature/about/src/main/res/drawable/ic_github_black_36dp.xml @@ -1,6 +1,6 @@ + android:paddingBottom="20dp"> diff --git a/feature/announcement/build.gradle b/feature/announcement/build.gradle index 91dfa9074..416ec97dc 100644 --- a/feature/announcement/build.gradle +++ b/feature/announcement/build.gradle @@ -45,6 +45,9 @@ dependencies { implementation Dep.Groupie.databinding testImplementation Dep.Test.junit + testImplementation Dep.Test.kotlinTestAssertions + testImplementation Dep.MockK.jvm + testImplementation project(':frontendcomponent:androidtestcomponent') androidTestImplementation Dep.Test.testRunner androidTestImplementation Dep.Test.espressoCore } diff --git a/feature/announcement/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/announcement/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..332b5c131 100644 --- a/feature/announcement/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/announcement/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.announcement.test", appContext.getPackageName()); } } diff --git a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt index 923e0f955..8d5192779 100644 --- a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt +++ b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt @@ -2,7 +2,7 @@ package io.github.droidkaigi.confsched2019.announcement.ui.actioncreator import androidx.lifecycle.Lifecycle import io.github.droidkaigi.confsched2019.action.Action -import io.github.droidkaigi.confsched2019.data.firestore.FireStore +import io.github.droidkaigi.confsched2019.data.firestore.Firestore import io.github.droidkaigi.confsched2019.di.PageScope import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher import io.github.droidkaigi.confsched2019.ext.android.coroutineScope @@ -14,7 +14,7 @@ import javax.inject.Inject class AnnouncementActionCreator @Inject constructor( override val dispatcher: Dispatcher, - private val fireStore: FireStore, + private val firestore: Firestore, @PageScope private val lifecycle: Lifecycle ) : CoroutineScope by lifecycle.coroutineScope, ErrorHandler { @@ -22,7 +22,7 @@ class AnnouncementActionCreator @Inject constructor( fun load() = launch { try { dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADING)) - dispatcher.dispatch(Action.AnnouncementLoaded(fireStore.getAnnouncements())) + dispatcher.dispatch(Action.AnnouncementLoaded(firestore.getAnnouncements())) dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADED)) } catch (e: Exception) { onError(e) diff --git a/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/AnnouncementDummyDatas.kt b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/AnnouncementDummyDatas.kt new file mode 100644 index 000000000..aecbfa097 --- /dev/null +++ b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/AnnouncementDummyDatas.kt @@ -0,0 +1,29 @@ +package io.github.droidkaigi.confsched2019 + +import com.soywiz.klock.DateTime +import com.soywiz.klock.minutes +import io.github.droidkaigi.confsched2019.model.Announcement + +private val startTime = DateTime.createAdjusted(2019, 2, 7, 10, 0) +fun dummyAnnouncementsData(): List { + return listOf( + Announcement( + "title1", + "content1", + startTime, + Announcement.Type.NOTIFICATION + ), + Announcement( + "title2", + "content2", + startTime + 30.minutes, + Announcement.Type.ALERT + ), + Announcement( + "title3", + "content3", + startTime + 60.minutes, + Announcement.Type.FEEDBACK + ) + ) +} diff --git a/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreatorTest.kt b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreatorTest.kt new file mode 100644 index 000000000..5547ccc47 --- /dev/null +++ b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreatorTest.kt @@ -0,0 +1,48 @@ +package io.github.droidkaigi.confsched2019.announcement.ui.actioncreator + +import androidx.lifecycle.Lifecycle +import io.github.droidkaigi.confsched2019.action.Action +import io.github.droidkaigi.confsched2019.data.firestore.Firestore +import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher +import io.github.droidkaigi.confsched2019.dummyAnnouncementsData +import io.github.droidkaigi.confsched2019.ext.android.CoroutinePlugin +import io.github.droidkaigi.confsched2019.model.LoadingState +import io.github.droidkaigi.confsched2019.widget.component.TestLifecycleOwner +import io.mockk.MockKAnnotations +import io.mockk.Ordering +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.RelaxedMockK +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + +class AnnouncementActionCreatorTest { + @RelaxedMockK lateinit var dispatcher: Dispatcher + @RelaxedMockK lateinit var firestore: Firestore + + @Before fun setUp() { + MockKAnnotations.init(this, relaxUnitFun = true) + CoroutinePlugin.mainDispatcherHandler = { Dispatchers.Default } + } + + @Test fun load() = runBlocking { + val lifecycleOwner = TestLifecycleOwner().handleEvent(Lifecycle.Event.ON_RESUME) + coEvery { firestore.getAnnouncements() } returns dummyAnnouncementsData() + val announcementActionCreator = AnnouncementActionCreator( + dispatcher, + firestore, + lifecycleOwner.lifecycle + ) + + announcementActionCreator.load() + + coVerify(ordering = Ordering.SEQUENCE) { + dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADING)) + firestore.getAnnouncements() + dispatcher.dispatch(Action.AnnouncementLoaded(dummyAnnouncementsData())) + dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADED)) + } + } +} diff --git a/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/store/AnnouncementStoreTest.kt b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/store/AnnouncementStoreTest.kt new file mode 100644 index 000000000..9e75e3008 --- /dev/null +++ b/feature/announcement/src/test/java/io/github/droidkaigi/confsched2019/announcement/ui/store/AnnouncementStoreTest.kt @@ -0,0 +1,60 @@ +package io.github.droidkaigi.confsched2019.announcement.ui.store + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.github.droidkaigi.confsched2019.action.Action +import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher +import io.github.droidkaigi.confsched2019.dummyAnnouncementsData +import io.github.droidkaigi.confsched2019.ext.android.CoroutinePlugin +import io.github.droidkaigi.confsched2019.ext.android.changedForever +import io.github.droidkaigi.confsched2019.model.Announcement +import io.github.droidkaigi.confsched2019.model.LoadingState +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verifySequence +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AnnouncementStoreTest { + @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before fun setUp() { + MockKAnnotations.init(this, relaxUnitFun = true) + CoroutinePlugin.mainDispatcherHandler = { Dispatchers.Default } + } + + @Test fun loadingState() = runBlocking { + val dispatcher = Dispatcher() + val announcementStore = AnnouncementStore(dispatcher) + val observer = mockk<(LoadingState?) -> Unit>(relaxed = true) + + announcementStore.loadingState.changedForever(observer) + + dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADING)) + dispatcher.dispatch(Action.AnnouncementLoadingStateChanged(LoadingState.LOADED)) + + verifySequence { + observer(LoadingState.LOADING) + observer(LoadingState.LOADED) + } + } + + @Test fun announcements() = runBlocking { + val dispatcher = Dispatcher() + val announcementStore = AnnouncementStore(dispatcher) + val observer: (List) -> Unit = mockk(relaxed = true) + announcementStore.announcements.changedForever(observer) + val dummyAnnouncements = dummyAnnouncementsData() + + dispatcher.dispatch( + Action.AnnouncementLoaded(dummyAnnouncements) + ) + + verifySequence { + observer(emptyList()) + observer(dummyAnnouncements) + } + } +} diff --git a/feature/floormap/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/floormap/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..9057d1743 100644 --- a/feature/floormap/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/floormap/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.floormap.test", appContext.getPackageName()); } } diff --git a/feature/floormap/src/main/res/drawable-hdpi/ic_floor1.png b/feature/floormap/src/main/res/drawable-hdpi/ic_floor1.png new file mode 100644 index 000000000..a31fd35d0 Binary files /dev/null and b/feature/floormap/src/main/res/drawable-hdpi/ic_floor1.png differ diff --git a/feature/floormap/src/main/res/drawable-hdpi/ic_floor2.png b/feature/floormap/src/main/res/drawable-hdpi/ic_floor2.png new file mode 100644 index 000000000..bffc412de Binary files /dev/null and b/feature/floormap/src/main/res/drawable-hdpi/ic_floor2.png differ diff --git a/feature/floormap/src/main/res/drawable-xhdpi/ic_floor1.png b/feature/floormap/src/main/res/drawable-xhdpi/ic_floor1.png new file mode 100644 index 000000000..ad9f52c2e Binary files /dev/null and b/feature/floormap/src/main/res/drawable-xhdpi/ic_floor1.png differ diff --git a/feature/floormap/src/main/res/drawable-xhdpi/ic_floor2.png b/feature/floormap/src/main/res/drawable-xhdpi/ic_floor2.png new file mode 100644 index 000000000..911174998 Binary files /dev/null and b/feature/floormap/src/main/res/drawable-xhdpi/ic_floor2.png differ diff --git a/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor1.png b/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor1.png new file mode 100644 index 000000000..4656cca94 Binary files /dev/null and b/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor1.png differ diff --git a/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor2.png b/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor2.png new file mode 100644 index 000000000..b2a5a925b Binary files /dev/null and b/feature/floormap/src/main/res/drawable-xxhdpi/ic_floor2.png differ diff --git a/feature/session/build.gradle b/feature/session/build.gradle index 25229d58f..90b84022a 100644 --- a/feature/session/build.gradle +++ b/feature/session/build.gradle @@ -27,7 +27,6 @@ dependencies { api Dep.AndroidX.emoji implementation Dep.AndroidX.design - api Dep.AndroidX.lifecycleExtensions api Dep.AndroidX.Navigation.runtime api Dep.AndroidX.Navigation.runtimeKtx @@ -49,6 +48,8 @@ dependencies { implementation Dep.Picasso.picasso implementation Dep.Picasso.picassoTransformation + implementation Dep.Timber.android + testImplementation Dep.Test.junit testImplementation Dep.Test.kotlinTestAssertions testImplementation Dep.MockK.jvm diff --git a/feature/session/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/session/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..e2ee118c1 100644 --- a/feature/session/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/session/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.session.test", appContext.getPackageName()); } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetDaySessionsFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetDaySessionsFragment.kt index 3c53885b7..c339e2635 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetDaySessionsFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetDaySessionsFragment.kt @@ -40,6 +40,7 @@ class BottomSheetDaySessionsFragment : DaggerFragment() { @Inject lateinit var sessionPageFragmentProvider: Provider @Inject lateinit var speechSessionItemFactory: SpeechSessionItem.Factory @Inject lateinit var sessionPageStoreFactory: SessionPageStore.Factory + @Inject lateinit var serviceSessionItemFactory: ServiceSessionItem.Factory private val sessionPageStore: SessionPageStore by lazy { sessionPageFragmentProvider.get().injectedViewModelProvider .get(SessionPageStore::class.java.name) { @@ -93,11 +94,13 @@ class BottomSheetDaySessionsFragment : DaggerFragment() { true ) is Session.ServiceSession -> - ServiceSessionItem(session) + serviceSessionItemFactory.create(session) } } groupAdapter.update(items) + binding.shouldShowEmptyStateView = false + val titleText = items .asSequence() .filterIsInstance() @@ -106,6 +109,9 @@ class BottomSheetDaySessionsFragment : DaggerFragment() { ?.startDayText ?: return@changed binding.sessionsBottomSheetTitle.text = titleText } + sessionPagesStore.filters.changed(viewLifecycleOwner) { + binding.isFiltered = it.isFiltered() + } sessionPageStore.filterSheetState.changed(viewLifecycleOwner) { newState -> if (newState == BottomSheetBehavior.STATE_EXPANDED || newState == BottomSheetBehavior.STATE_COLLAPSED diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetFavoriteSessionsFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetFavoriteSessionsFragment.kt index b8e8abb9a..749e14e68 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetFavoriteSessionsFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/BottomSheetFavoriteSessionsFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager @@ -12,15 +11,19 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.AutoTransition import androidx.transition.TransitionManager import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.Item import com.xwray.groupie.databinding.ViewHolder import dagger.Module import dagger.Provides import io.github.droidkaigi.confsched2019.ext.android.changed +import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.model.SessionPage import io.github.droidkaigi.confsched2019.session.R import io.github.droidkaigi.confsched2019.session.databinding.FragmentBottomSheetSessionsBinding import io.github.droidkaigi.confsched2019.session.ui.actioncreator.SessionContentsActionCreator import io.github.droidkaigi.confsched2019.session.ui.actioncreator.SessionPageActionCreator +import io.github.droidkaigi.confsched2019.session.ui.item.ServiceSessionItem +import io.github.droidkaigi.confsched2019.session.ui.item.SessionItem import io.github.droidkaigi.confsched2019.session.ui.item.SpeechSessionItem import io.github.droidkaigi.confsched2019.session.ui.store.SessionContentsStore import io.github.droidkaigi.confsched2019.session.ui.store.SessionPageStore @@ -41,6 +44,7 @@ class BottomSheetFavoriteSessionsFragment : DaggerFragment() { @Inject lateinit var sessionPageActionCreator: SessionPageActionCreator @Inject lateinit var sessionPageFragmentProvider: Provider @Inject lateinit var speechSessionItemFactory: SpeechSessionItem.Factory + @Inject lateinit var serviceSessionItemFactory: ServiceSessionItem.Factory @Inject lateinit var sessionDetailStoreFactory: SessionPageStore.Factory private val sessionPageStore: SessionPageStore by lazy { @@ -89,18 +93,27 @@ class BottomSheetFavoriteSessionsFragment : DaggerFragment() { sessionPagesStore.filteredFavoritedSessions().changed(viewLifecycleOwner) { sessions -> val items = sessions - .map { session -> - speechSessionItemFactory.create( - session, - SessionPagesFragmentDirections.actionSessionToSessionDetail( - session.id - ), - true - ) + .map> { session -> + when (session) { + is Session.SpeechSession -> + speechSessionItemFactory.create( + session, + SessionPagesFragmentDirections.actionSessionToSessionDetail( + session.id + ), + true + ) + is Session.ServiceSession -> + serviceSessionItemFactory.create(session) + } } groupAdapter.update(items) applyTitleText() + binding.shouldShowEmptyStateView = items.isEmpty() + } + sessionPagesStore.filters.changed(viewLifecycleOwner) { + binding.isFiltered = it.isFiltered() } sessionPageStore.filterSheetState.changed(viewLifecycleOwner) { newState -> if (newState == BottomSheetBehavior.STATE_EXPANDED || @@ -112,8 +125,7 @@ class BottomSheetFavoriteSessionsFragment : DaggerFragment() { excludeChildren(binding.sessionsRecycler, true) }) val isCollapsed = newState == BottomSheetBehavior.STATE_COLLAPSED - binding.sessionsBottomSheetShowFilterButton.isVisible = !isCollapsed - binding.sessionsBottomSheetHideFilterButton.isVisible = isCollapsed + binding.isCollapsed = isCollapsed } } } @@ -125,7 +137,7 @@ class BottomSheetFavoriteSessionsFragment : DaggerFragment() { return } binding.sessionsBottomSheetTitle.text = (groupAdapter - .getItem(firstPosition) as SpeechSessionItem) + .getItem(firstPosition) as SessionItem) .session .startDayText } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SearchFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SearchFragment.kt index 4d90f7006..91bad4ba3 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SearchFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SearchFragment.kt @@ -6,7 +6,9 @@ import android.view.Menu import android.view.MenuInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle @@ -37,6 +39,7 @@ class SearchFragment : DaggerFragment() { @Inject lateinit var searchActionCreator: SearchActionCreator @Inject lateinit var speechSessionItemFactory: SpeechSessionItem.Factory + @Inject lateinit var serviceSessionItemFactory: ServiceSessionItem.Factory @Inject lateinit var sessionContentsStore: SessionContentsStore private var searchView: SearchView? = null @Inject lateinit var searchStoreProvider: Provider @@ -93,7 +96,7 @@ class SearchFragment : DaggerFragment() { false ) is Session.ServiceSession -> - ServiceSessionItem(session) + serviceSessionItemFactory.create(session) } } groupAdapter.update(items) @@ -127,6 +130,14 @@ class SearchFragment : DaggerFragment() { searchView.setOnCloseListener { false } } } + + override fun onPause() { + super.onPause() + + val imm = ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java) + val view = activity?.currentFocus + imm?.hideSoftInputFromWindow(view?.windowToken, 0) + } } @Module diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionDetailFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionDetailFragment.kt index 50947f693..3161a61ea 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionDetailFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionDetailFragment.kt @@ -5,8 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import com.xwray.groupie.GroupAdapter import com.xwray.groupie.databinding.ViewHolder @@ -15,6 +15,7 @@ import dagger.Provides import dagger.android.support.AndroidSupportInjection import io.github.droidkaigi.confsched2019.di.PageScope import io.github.droidkaigi.confsched2019.ext.android.changed +import io.github.droidkaigi.confsched2019.model.LoadingState import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.model.defaultLang import io.github.droidkaigi.confsched2019.session.R @@ -23,7 +24,9 @@ import io.github.droidkaigi.confsched2019.session.ui.actioncreator.SessionConten import io.github.droidkaigi.confsched2019.session.ui.item.SpeakerItem import io.github.droidkaigi.confsched2019.session.ui.store.SessionContentsStore import io.github.droidkaigi.confsched2019.session.ui.widget.DaggerFragment +import io.github.droidkaigi.confsched2019.system.actioncreator.ActivityActionCreator import io.github.droidkaigi.confsched2019.system.store.SystemStore +import io.github.droidkaigi.confsched2019.util.ProgressTimeLatch import javax.inject.Inject class SessionDetailFragment : DaggerFragment() { @@ -33,6 +36,9 @@ class SessionDetailFragment : DaggerFragment() { @Inject lateinit var systemStore: SystemStore @Inject lateinit var sessionContentsStore: SessionContentsStore @Inject lateinit var speakerItemFactory: SpeakerItem.Factory + @Inject lateinit var activityActionCreator: ActivityActionCreator + + private lateinit var progressTimeLatch: ProgressTimeLatch private lateinit var sessionDetailFragmentArgs: SessionDetailFragmentArgs private val groupAdapter = GroupAdapter>() @@ -62,12 +68,13 @@ class SessionDetailFragment : DaggerFragment() { binding.bottomAppBar.replaceMenu(R.menu.menu_session_detail_bottomappbar) binding.bottomAppBar.setOnMenuItemClickListener { item -> when (item.itemId) { - R.id.session_share -> - Toast.makeText( - requireContext(), - "not implemented yet", - Toast.LENGTH_SHORT - ).show() + R.id.session_share -> { + val session = binding.session ?: return@setOnMenuItemClickListener false + activityActionCreator.shareUrl(getString( + R.string.session_detail_share_url, + session.id + )) + } R.id.session_place -> Toast.makeText( requireContext(), @@ -82,8 +89,17 @@ class SessionDetailFragment : DaggerFragment() { .changed(viewLifecycleOwner) { session -> applySessionLayout(session) } + + progressTimeLatch = ProgressTimeLatch { showProgress -> + binding.progressBar.isVisible = showProgress + } + sessionContentsStore.loadingState.changed(viewLifecycleOwner) { + progressTimeLatch.loading = it == LoadingState.LOADING + } + binding.sessionFavorite.setOnClickListener { val session = binding.session ?: return@setOnClickListener + progressTimeLatch.loading = true sessionContentsActionCreator.toggleFavorite(session) } } @@ -97,8 +113,13 @@ class SessionDetailFragment : DaggerFragment() { session.timeInMinutes, session.room.name ) + binding.sessionIntendedAudienceDescription.text = session.intendedAudience binding.categoryChip.text = session.category.name.getByLang(systemStore.lang) + session.message?.let { message -> + binding.sessionMessage.text = message.getByLang(systemStore.lang) + } + val sessionItems = session .speakers .map { @@ -108,6 +129,17 @@ class SessionDetailFragment : DaggerFragment() { ) } groupAdapter.update(sessionItems) + + binding.sessionVideoButton.setOnClickListener { + session.videoUrl?.let { urlString -> + activityActionCreator.openUrl(urlString) + } + } + binding.sessionSlideButton.setOnClickListener { + session.slideUrl?.let { urlString -> + activityActionCreator.openUrl(urlString) + } + } } } @@ -121,11 +153,5 @@ abstract class SessionDetailFragmentModule { fun providesLifecycle(sessionsFragment: SessionDetailFragment): Lifecycle { return sessionsFragment.viewLifecycleOwner.lifecycle } - - @JvmStatic @Provides fun provideActivity( - sessionsFragment: SessionDetailFragment - ): FragmentActivity { - return sessionsFragment.requireActivity() - } } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPageFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPageFragment.kt index ceb2b2d51..0637e7b89 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPageFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPageFragment.kt @@ -35,6 +35,8 @@ import io.github.droidkaigi.confsched2019.session.ui.store.SessionPagesStore import io.github.droidkaigi.confsched2019.session.ui.widget.DaggerFragment import io.github.droidkaigi.confsched2019.system.store.SystemStore import io.github.droidkaigi.confsched2019.widget.BottomSheetBehavior +import io.github.droidkaigi.confsched2019.widget.FilterChip +import io.github.droidkaigi.confsched2019.widget.onCheckedChanged import me.tatarka.injectedvmprovider.InjectedViewModelProviders import me.tatarka.injectedvmprovider.ktx.injectedViewModelProvider import javax.inject.Inject @@ -68,6 +70,13 @@ class SessionPageFragment : DaggerFragment() { private val bottomSheetBehavior: BottomSheetBehavior<*> get() = BottomSheetBehavior.from(binding.sessionsSheet) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + setupSessionsFragment() + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -82,7 +91,7 @@ class SessionPageFragment : DaggerFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - setupBottomSheet(savedInstanceState) + setupBottomSheetBehavior() binding.sessionsFilterReset.setOnClickListener { sessionPagesActionCreator.clearFilters() @@ -120,27 +129,29 @@ class SessionPageFragment : DaggerFragment() { } } - private fun setupBottomSheet(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - val fragment: Fragment = when (val tab = SessionPage.pages[args.tabIndex]) { - is SessionPage.Day -> { - BottomSheetDaySessionsFragment.newInstance( - BottomSheetDaySessionsFragmentArgs - .Builder(tab.day) - .build() - ) - } - SessionPage.Favorite -> { - BottomSheetFavoriteSessionsFragment.newInstance() - } + private fun setupSessionsFragment() { + val tab = SessionPage.pages[args.tabIndex] + val fragment: Fragment = when (tab) { + is SessionPage.Day -> { + BottomSheetDaySessionsFragment.newInstance( + BottomSheetDaySessionsFragmentArgs + .Builder(tab.day) + .build() + ) + } + SessionPage.Favorite -> { + BottomSheetFavoriteSessionsFragment.newInstance() } - - childFragmentManager - .beginTransaction() - .replace(R.id.sessions_sheet, fragment) - .disallowAddToBackStack() - .commit() } + + childFragmentManager + .beginTransaction() + .replace(R.id.sessions_sheet, fragment, tab.title) + .disallowAddToBackStack() + .commit() + } + + private fun setupBottomSheetBehavior() { bottomSheetBehavior.isHideable = false binding.sessionsSheet.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { @@ -176,7 +187,7 @@ class SessionPageFragment : DaggerFragment() { R.layout.layout_chip, this, false - ) as Chip + ) as FilterChip chip.apply { text = chipText(item) tag = item @@ -191,43 +202,43 @@ class SessionPageFragment : DaggerFragment() { private fun applyFilters() { val filterRooms = sessionPagesStore.filtersValue.rooms binding.sessionsFilterRoomChip.forEach { - val chip = it as? Chip ?: return@forEach + val chip = it as? FilterChip ?: return@forEach val room = it.tag as? Room ?: return@forEach - chip.setOnCheckedChangeListener(null) + chip.onCheckedChangeListener = null if (filterRooms.isNotEmpty()) { chip.isChecked = filterRooms.contains(room) } else { chip.isChecked = false } - chip.setOnCheckedChangeListener { _, isChecked -> + chip.onCheckedChanged { _, isChecked -> sessionPagesActionCreator.changeFilter(room, isChecked) } } val filterCategorys = sessionPagesStore.filtersValue.categories binding.sessionsFilterCategoryChip.forEach { - val chip = it as? Chip ?: return@forEach + val chip = it as? FilterChip ?: return@forEach val category = it.tag as? Category ?: return@forEach - chip.setOnCheckedChangeListener(null) + chip.onCheckedChangeListener = null if (filterCategorys.isNotEmpty()) { chip.isChecked = filterCategorys.contains(category) } else { chip.isChecked = false } - chip.setOnCheckedChangeListener { _, isChecked -> + chip.onCheckedChanged { _, isChecked -> sessionPagesActionCreator.changeFilter(category, isChecked) } } val filterLangs = sessionPagesStore.filtersValue.langs binding.sessionsFilterLangChip.forEach { - val chip = it as? Chip ?: return@forEach + val chip = it as? FilterChip ?: return@forEach val lang = it.tag as? Lang ?: return@forEach - chip.setOnCheckedChangeListener(null) + chip.onCheckedChangeListener = null if (filterLangs.isNotEmpty()) { chip.isChecked = filterLangs.contains(lang) } else { chip.isChecked = false } - chip.setOnCheckedChangeListener { _, isChecked -> + chip.onCheckedChanged { _, isChecked -> sessionPagesActionCreator.changeFilter(lang, isChecked) } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPagesFragment.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPagesFragment.kt index 2565f7686..1817cfeab 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPagesFragment.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/SessionPagesFragment.kt @@ -2,6 +2,8 @@ package io.github.droidkaigi.confsched2019.session.ui import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible @@ -10,7 +12,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentStatePagerAdapter import androidx.lifecycle.Lifecycle import androidx.viewpager.widget.ViewPager -import com.google.android.material.chip.Chip import dagger.Module import dagger.Provides import dagger.android.ContributesAndroidInjector @@ -51,6 +52,7 @@ class SessionPagesFragment : DaggerFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + setHasOptionsMenu(true) binding = DataBindingUtil.inflate( inflater, R.layout.fragment_session_pages, @@ -107,18 +109,11 @@ class SessionPagesFragment : DaggerFragment() { } } ) + } - (0 until binding.sessionsTabLayout.tabCount).forEach { - val view = layoutInflater.inflate( - R.layout.layout_title_chip, binding.sessionsTabLayout, false - ) as ViewGroup - val chip = view.getChildAt(0) as Chip - val tab = binding.sessionsTabLayout.getTabAt(it) - tab?.let { - chip.text = tab.text - tab.setCustomView(view) - } - } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_toolbar, menu) } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt index c19170d9a..e7b2d0d71 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt @@ -9,9 +9,10 @@ import io.github.droidkaigi.confsched2019.ext.android.coroutineScope import io.github.droidkaigi.confsched2019.model.LoadingState import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler -import io.github.droidkaigi.confsched2019.util.logd import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber +import timber.log.debug import javax.inject.Inject @PageScope @@ -23,22 +24,22 @@ class SessionContentsActionCreator @Inject constructor( ErrorHandler { fun refresh() = launch { try { - logd { "SessionContentsActionCreator: refresh start" } + Timber.debug { "SessionContentsActionCreator: refresh start" } dispatcher.dispatchLoadingState(LoadingState.LOADING) - logd { "SessionContentsActionCreator: At first, load db data" } + Timber.debug { "SessionContentsActionCreator: At first, load db data" } // At first, load db data val sessionContents = sessionRepository.sessionContents() dispatcher.dispatch(Action.SessionContentsLoaded(sessionContents)) // fetch api data - logd { "SessionContentsActionCreator: fetch api data" } + Timber.debug { "SessionContentsActionCreator: fetch api data" } sessionRepository.refresh() // reload db data - logd { "SessionContentsActionCreator: reload db data" } + Timber.debug { "SessionContentsActionCreator: reload db data" } val refreshedSessionContents = sessionRepository.sessionContents() dispatcher.dispatch(Action.SessionContentsLoaded(refreshedSessionContents)) - logd { "SessionContentsActionCreator: refresh end" } + Timber.debug { "SessionContentsActionCreator: refresh end" } dispatcher.dispatchLoadingState(LoadingState.LOADED) } catch (e: Exception) { onError(e) @@ -60,7 +61,7 @@ class SessionContentsActionCreator @Inject constructor( } } - fun toggleFavorite(session: Session.SpeechSession) { + fun toggleFavorite(session: Session) { launch { try { dispatcher.dispatchLoadingState(LoadingState.LOADING) diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/ServiceSessionItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/ServiceSessionItem.kt index ed3839e6a..059080479 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/ServiceSessionItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/ServiceSessionItem.kt @@ -1,31 +1,49 @@ package io.github.droidkaigi.confsched2019.session.ui.item +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.xwray.groupie.databinding.BindableItem import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.session.R -import io.github.droidkaigi.confsched2019.session.databinding.ItemSpecialSessionBinding +import io.github.droidkaigi.confsched2019.session.databinding.ItemServiceSessionBinding +import io.github.droidkaigi.confsched2019.session.ui.actioncreator.SessionContentsActionCreator -class ServiceSessionItem( - override val session: Session.ServiceSession -) : BindableItem( +class ServiceSessionItem @AssistedInject constructor( + @Assisted override val session: Session.ServiceSession, + sessionContentsActionCreator: SessionContentsActionCreator +) : BindableItem( session.id.hashCode().toLong() ), SessionItem { - val specialSession get() = session + val serviceSession get() = session - override fun bind(viewBinding: ItemSpecialSessionBinding, position: Int) { + @AssistedInject.Factory + interface Factory { + fun create( + session: Session.ServiceSession + ): ServiceSessionItem + } + + private val onFavoriteClickListener: (Session.ServiceSession) -> Unit = { session -> + sessionContentsActionCreator.toggleFavorite(session) + } + + override fun bind(viewBinding: ItemServiceSessionBinding, position: Int) { with(viewBinding) { - session = specialSession + session = serviceSession @Suppress("StringFormatMatches") // FIXME timeAndRoom.text = root.context.getString( R.string.session_duration_room_format, - specialSession.timeInMinutes, - specialSession.room.name + serviceSession.timeInMinutes, + serviceSession.room.name ) + favorite.setOnClickListener { + onFavoriteClickListener(serviceSession) + } } } - override fun getLayout(): Int = R.layout.item_special_session + override fun getLayout(): Int = R.layout.item_service_session override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/SpeechSessionItem.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/SpeechSessionItem.kt index 546004a1f..628740d5b 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/SpeechSessionItem.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/item/SpeechSessionItem.kt @@ -1,9 +1,11 @@ package io.github.droidkaigi.confsched2019.session.ui.item import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -13,6 +15,8 @@ import androidx.navigation.NavDirections import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import com.squareup.picasso.Picasso +import com.squareup.picasso.Target import com.xwray.groupie.databinding.BindableItem import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.model.Speaker @@ -20,9 +24,9 @@ import io.github.droidkaigi.confsched2019.model.defaultLang import io.github.droidkaigi.confsched2019.session.R import io.github.droidkaigi.confsched2019.session.databinding.ItemSessionBinding import io.github.droidkaigi.confsched2019.session.ui.actioncreator.SessionContentsActionCreator -import io.github.droidkaigi.confsched2019.session.ui.bindingadapter.loadImage import io.github.droidkaigi.confsched2019.system.store.SystemStore import io.github.droidkaigi.confsched2019.util.lazyWithParam +import jp.wasabeef.picasso.transformations.CropCircleTransformation import kotlin.math.max class SpeechSessionItem @AssistedInject constructor( @@ -79,7 +83,7 @@ class SpeechSessionItem @AssistedInject constructor( bindSpeaker() speechSession.message?.let { message -> - this@with.message.text = message.getBodyByLang(systemStore.lang) + this@with.message.text = message.getByLang(systemStore.lang) } } } @@ -102,11 +106,8 @@ class SpeechSessionItem @AssistedInject constructor( val speakerView = layoutInflater.get(root.context).inflate( R.layout.layout_speaker, speakers, false ) as ViewGroup - val imageView: ImageView = speakerView.findViewById( - R.id.speaker_image - ) val textView: TextView = speakerView.findViewById(R.id.speaker) - bindSpeakerData(speaker, textView, imageView) + bindSpeakerData(speaker, textView) speakers.addView(speakerView) return@forEach @@ -114,35 +115,74 @@ class SpeechSessionItem @AssistedInject constructor( if (existSpeakerView != null && speaker != null) { val textView: TextView = existSpeakerView.findViewById(R.id.speaker) textView.text = speaker.name - val imageView = existSpeakerView.findViewById(R.id.speaker_image) - bindSpeakerData(speaker, textView, imageView) + bindSpeakerData(speaker, textView) } } } private fun bindSpeakerData( speaker: Speaker, - textView: TextView, - imageView: ImageView + textView: TextView ) { textView.text = speaker.name - val context = imageView.context - val placeHolder = VectorDrawableCompat.create( - context.resources, - R.drawable.ic_person_outline_black_24dp, - null - ) - val placeHolderColor = ContextCompat.getColor( - context, - R.color.gray2 - ) - loadImage( - imageView = imageView, - imageUrl = speaker.imageUrl, - circleCrop = true, - rawPlaceHolder = placeHolder, - placeHolderTint = placeHolderColor - ) + val imageUrl = speaker.imageUrl + val context = textView.context + val placeHolder = run { + VectorDrawableCompat.create( + context.resources, + R.drawable.ic_person_outline_black_24dp, + null + )?.apply { + setTint( + ContextCompat.getColor( + context, + R.color.gray2 + ) + ) + } + } + + imageUrl ?: run { + placeHolder?.let { + textView.setLeftDrawable(it) + } + } + + Picasso + .get() + .load(imageUrl) + .transform(CropCircleTransformation()) + .apply { + if (placeHolder != null) { + placeholder(placeHolder) + } + } + .into(object : Target { + override fun onPrepareLoad(placeHolderDrawable: Drawable?) { + placeHolderDrawable?.let { + textView.setLeftDrawable(it) + } + } + + override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) { + } + + override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) { + val res = textView.context.resources + val drawable = BitmapDrawable(res, bitmap) + textView.setLeftDrawable(drawable) + } + }) + } + + fun TextView.setLeftDrawable(drawable: Drawable) { + val res = context.resources + val widthDp = 16 + val heightDp = 16 + val widthPx = (widthDp * res.displayMetrics.density).toInt() + val heightPx = (heightDp * res.displayMetrics.density).toInt() + drawable.setBounds(0, 0, widthPx, heightPx) + setCompoundDrawables(drawable, null, null, null) } override fun getLayout(): Int = R.layout.item_session diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionPagesStore.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionPagesStore.kt index 6969acea7..258cf81e3 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionPagesStore.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionPagesStore.kt @@ -94,10 +94,10 @@ class SessionPagesStore @Inject constructor( } } - fun filteredFavoritedSessions(): LiveData> { + fun filteredFavoritedSessions(): LiveData> { return filteredSessions .map { sessions -> - sessions.orEmpty().filterIsInstance() + sessions.orEmpty() .filter { session -> session.isFavorited } } } diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionBottomAppBarBehavior.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionBottomAppBarBehavior.kt new file mode 100644 index 000000000..5eb09b02e --- /dev/null +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionBottomAppBarBehavior.kt @@ -0,0 +1,57 @@ +package io.github.droidkaigi.confsched2019.session.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.children +import androidx.core.widget.NestedScrollView +import com.google.android.material.bottomappbar.BottomAppBar + +class SessionBottomAppBarBehavior( + context: Context? = null, + attrs: AttributeSet? = null +) : BottomAppBar.Behavior(context, attrs) { + + private var state: Int = STATE_SCROLLED_UP + + override fun onInterceptTouchEvent( + parent: CoordinatorLayout, + child: BottomAppBar, + ev: MotionEvent + ): Boolean { + if (!child.isShown || ev.action != MotionEvent.ACTION_DOWN) { + return super.onInterceptTouchEvent(parent, child, ev) + } + + // If screen be able not to scrolled, it makes to slide the bottom app bar. + if (!canScroll(parent)) { + state = if (state == STATE_SCROLLED_UP) { + super.slideDown(child) + STATE_SCROLLED_DOWN + } else { + super.slideUp(child) + STATE_SCROLLED_UP + } + } + return super.onInterceptTouchEvent(parent, child, ev) + } + + private fun canScroll(parent: CoordinatorLayout): Boolean { + val nestedScrollView = if (parent.childCount > 0) { + (parent.children.find { it is NestedScrollView } as? NestedScrollView) + } else { + null + } + return nestedScrollView?.canScrollVertically(POSITIVE_DIRECTION) ?: true || + nestedScrollView?.canScrollVertically(NEGATIVE_DIRECTION) ?: true + } + + companion object { + private const val POSITIVE_DIRECTION = 1 + private const val NEGATIVE_DIRECTION = -1 + + private const val STATE_SCROLLED_DOWN = 1 + private const val STATE_SCROLLED_UP = 2 + } +} diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionsItemDecoration.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionsItemDecoration.kt index fd2121f94..27317cbc1 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionsItemDecoration.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/widget/SessionsItemDecoration.kt @@ -5,12 +5,16 @@ import android.content.res.Resources import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint +import android.util.SparseArray +import android.view.View import androidx.core.content.res.ResourcesCompat +import androidx.core.util.forEach import androidx.recyclerview.widget.RecyclerView import com.xwray.groupie.GroupAdapter import io.github.droidkaigi.confsched2019.session.R import io.github.droidkaigi.confsched2019.session.ui.item.SessionItem -import io.github.droidkaigi.confsched2019.util.logd +import io.github.droidkaigi.confsched2019.timber.debug +import timber.log.Timber class SessionsItemDecoration( val context: Context, @@ -29,6 +33,8 @@ class SessionsItemDecoration( private val textPaddingBottom = resources.getDimensionPixelSize( R.dimen.session_bottom_sheet_left_time_text_padding_bottom ) + // Keep SparseArray instance on property to avoid object creation in every onDrawOver() + private val adapterPositionToViews = SparseArray() val paint = Paint().apply { style = Paint.Style.FILL @@ -38,20 +44,25 @@ class SessionsItemDecoration( try { typeface = ResourcesCompat.getFont(context, R.font.lekton) } catch (e: Resources.NotFoundException) { - logd(e = e) + Timber.debug(e) } } override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - var lastTime: String? = null + // Sort child views by adapter position for (i in 0 until parent.childCount) { val view = parent.getChildAt(i) val position = parent.getChildAdapterPosition(view) - if (position == -1 || position >= groupAdapter.itemCount) return + if (position != RecyclerView.NO_POSITION && position < groupAdapter.itemCount) { + adapterPositionToViews.put(position, view) + } + } - val time = getSessionTime(position) ?: continue + var lastTime: String? = null + adapterPositionToViews.forEach { position, view -> + val time = getSessionTime(position) ?: return@forEach - if (lastTime == time) continue + if (lastTime == time) return@forEach lastTime = time val nextTime = getSessionTime(position + 1) @@ -68,6 +79,8 @@ class SessionsItemDecoration( paint ) } + + adapterPositionToViews.clear() } private fun getSessionTime(position: Int): String? { diff --git a/feature/session/src/main/res/color/selector_tab_title.xml b/feature/session/src/main/res/color/selector_tab_title.xml deleted file mode 100644 index b9e75233c..000000000 --- a/feature/session/src/main/res/color/selector_tab_title.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/feature/session/src/main/res/drawable/ic_file_copy_outline_gray2_24dp.xml b/feature/session/src/main/res/drawable/ic_file_copy_outline_gray2_24dp.xml new file mode 100644 index 000000000..51c3c69cf --- /dev/null +++ b/feature/session/src/main/res/drawable/ic_file_copy_outline_gray2_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/session/src/main/res/drawable/ic_local_movies_outline_gray2_24dp.xml b/feature/session/src/main/res/drawable/ic_local_movies_outline_gray2_24dp.xml new file mode 100644 index 000000000..a92fb656b --- /dev/null +++ b/feature/session/src/main/res/drawable/ic_local_movies_outline_gray2_24dp.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/session/src/main/res/drawable/ic_room_outline_black_24dp.xml b/feature/session/src/main/res/drawable/ic_room_outline_black_24dp.xml new file mode 100644 index 000000000..64b261c45 --- /dev/null +++ b/feature/session/src/main/res/drawable/ic_room_outline_black_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/session/src/main/res/drawable/shape_pill_indicator.xml b/feature/session/src/main/res/drawable/shape_pill_indicator.xml new file mode 100644 index 000000000..c506c2fa6 --- /dev/null +++ b/feature/session/src/main/res/drawable/shape_pill_indicator.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/feature/session/src/main/res/layout/fragment_bottom_sheet_sessions.xml b/feature/session/src/main/res/layout/fragment_bottom_sheet_sessions.xml index f573dde03..765c4fb50 100644 --- a/feature/session/src/main/res/layout/fragment_bottom_sheet_sessions.xml +++ b/feature/session/src/main/res/layout/fragment_bottom_sheet_sessions.xml @@ -8,9 +8,19 @@ + name="isCollapsed" + type="Boolean" + /> + + + + @@ -22,12 +32,29 @@ tools:context="io.github.droidkaigi.confsched2019.session.ui.SessionPagesFragment" > + + + + + + + + + + + diff --git a/feature/session/src/main/res/layout/fragment_session_detail.xml b/feature/session/src/main/res/layout/fragment_session_detail.xml index ad37665d8..fe330270b 100644 --- a/feature/session/src/main/res/layout/fragment_session_detail.xml +++ b/feature/session/src/main/res/layout/fragment_session_detail.xml @@ -34,6 +34,14 @@ android:layout_height="match_parent" > + + + + + + - + + + + + + @@ -173,15 +258,15 @@ style="@style/Widget.MaterialComponents.Chip.Action" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:clickable="false" + android:enabled="false" + android:focusable="false" android:text="@{session.language.getByLang(lang)}" android:textAppearance="?textAppearanceCaption" android:textColor="@color/white" app:chipBackgroundColor="#7982e1" app:chipMinTouchTargetSize="0dp" tools:text="Japanese" - android:clickable="false" - android:enabled="false" - android:focusable="false" /> @@ -237,9 +325,9 @@ android:layout_marginTop="26dp" android:text="@string/session_detail_survey" android:textAppearance="@style/TextAppearance.App.Subtitle1" - app:visibleGone="@{session.isFinished}" app:layout_constraintStart_toStartOf="@id/session_title" - app:layout_constraintTop_toBottomOf="@id/divider3" + app:layout_constraintTop_toBottomOf="@id/divider_top_survey" + app:visibleGone="@{session.isFinished}" /> - + + + + + + @@ -319,6 +467,7 @@ android:layout_gravity="bottom" app:fabAlignmentMode="end" app:hideOnScroll="true" + app:layout_behavior="@string/session_bottom_appbar_behavior" /> @@ -93,12 +92,11 @@ @@ -118,12 +116,11 @@ diff --git a/feature/session/src/main/res/layout/fragment_session_pages.xml b/feature/session/src/main/res/layout/fragment_session_pages.xml index 15b0ea0db..0f048acef 100644 --- a/feature/session/src/main/res/layout/fragment_session_pages.xml +++ b/feature/session/src/main/res/layout/fragment_session_pages.xml @@ -8,6 +8,7 @@ @@ -15,20 +16,23 @@ android:id="@+id/sessions_tab_layout" style="@style/Widget.MaterialComponents.TabLayout.Colored" android:layout_width="0dp" - android:layout_height="wrap_content" - android:background="?colorPrimary" + android:layout_height="40dp" android:tabStripEnabled="false" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:tabIndicatorColor="@android:color/transparent" - app:tabRippleColor="@null" + app:tabGravity="fill" + app:tabIndicator="@drawable/shape_pill_indicator" + app:tabIndicatorGravity="stretch" + app:tabTextAppearance="@style/TabTextAppearance" + app:tabTextColor="@color/white" /> + + + + @@ -54,12 +58,26 @@ android:text="@{session.title}" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.App.Subtitle1" + app:layout_constraintEnd_toEndOf="@id/favorite" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/live" tools:text="テストセッション" /> + + - - diff --git a/feature/session/src/main/res/layout/layout_speaker.xml b/feature/session/src/main/res/layout/layout_speaker.xml index 52e6076fe..7ea443412 100644 --- a/feature/session/src/main/res/layout/layout_speaker.xml +++ b/feature/session/src/main/res/layout/layout_speaker.xml @@ -6,18 +6,11 @@ android:layout_height="wrap_content" > - - diff --git a/feature/session/src/main/res/layout/layout_title_chip.xml b/feature/session/src/main/res/layout/layout_title_chip.xml deleted file mode 100644 index a2ac5e0f8..000000000 --- a/feature/session/src/main/res/layout/layout_title_chip.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/frontend/android/src/main/res/menu/menu_toolbar.xml b/feature/session/src/main/res/menu/menu_toolbar.xml similarity index 100% rename from frontend/android/src/main/res/menu/menu_toolbar.xml rename to feature/session/src/main/res/menu/menu_toolbar.xml diff --git a/feature/session/src/main/res/values-ja/strings.xml b/feature/session/src/main/res/values-ja/strings.xml index 3be46132d..abc80bfd5 100644 --- a/feature/session/src/main/res/values-ja/strings.xml +++ b/feature/session/src/main/res/values-ja/strings.xml @@ -3,9 +3,16 @@ フィルター リセット 絞り込む + 絞り込み中 + 対象者 アンケート 回答する + 資料 + 動画 + スライド + https://droidkaigi.jp/2019/timetable/%s 検索する… アンケートに回答する 同時通訳 + お気に入りのセッションを登録すると\nあなた専用のプランを作ることが\nできます。 diff --git a/feature/session/src/main/res/values/dimens.xml b/feature/session/src/main/res/values/dimens.xml index 48038b1ba..3e6aa518c 100644 --- a/feature/session/src/main/res/values/dimens.xml +++ b/feature/session/src/main/res/values/dimens.xml @@ -8,5 +8,4 @@ 8dp 0dp 4dp - 34dp diff --git a/feature/session/src/main/res/values/strings.xml b/feature/session/src/main/res/values/strings.xml index be9a09d3f..b3b82f56e 100644 --- a/feature/session/src/main/res/values/strings.xml +++ b/feature/session/src/main/res/values/strings.xml @@ -5,15 +5,21 @@ header image %1$d min / %2$s Filtering + Filtered Filter Reset Room Language Category + Intended Audience Tags Survey Go to survey Speaker + Resources + Video + Slides + https://droidkaigi.jp/2019/en/timetable/%s Session Share Place @@ -21,4 +27,6 @@ Go to survey Live simultaneous interpretation + io.github.droidkaigi.confsched2019.session.ui.widget.SessionBottomAppBarBehavior + Add sessions to your plan\nby tapping the bookmark button. diff --git a/feature/session/src/main/res/values/styles.xml b/feature/session/src/main/res/values/styles.xml new file mode 100644 index 000000000..e6aef88f8 --- /dev/null +++ b/feature/session/src/main/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2019/SessionDummyDatas.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2019/SessionDummyDatas.kt index 722918416..7a51a46c0 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2019/SessionDummyDatas.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2019/SessionDummyDatas.kt @@ -6,7 +6,6 @@ import io.github.droidkaigi.confsched2019.model.Category import io.github.droidkaigi.confsched2019.model.LocaledString import io.github.droidkaigi.confsched2019.model.Room import io.github.droidkaigi.confsched2019.model.Session -import io.github.droidkaigi.confsched2019.model.SessionMessage import io.github.droidkaigi.confsched2019.model.SessionType private val startTime = DateTime.createAdjusted(2019, 2, 7, 10, 0) @@ -19,7 +18,8 @@ fun dummySessionData(): List { startTime + 30.minutes, "session", Room(0, "Hall"), - SessionType.WelcomeTalk + SessionType.WelcomeTalk, + true ), firstDummySpeechSession(), Session.SpeechSession( @@ -37,10 +37,12 @@ fun dummySessionData(): List { language = LocaledString("英語", "English"), category = Category(10, LocaledString("ツール", "Tool")), intendedAudience = "extream", + videoUrl = "https://droidkaigi.jp/2019/#might_be_null", + slideUrl = "https://droidkaigi.jp/2019/#might_be_null", isInterpretationTarget = true, isFavorited = true, speakers = listOf(), - message = SessionMessage("部屋移動", "room moved") + message = LocaledString("部屋移動", "room moved") ) ) } @@ -61,6 +63,8 @@ fun firstDummySpeechSession(): Session.SpeechSession { language = LocaledString("日本語", "Japanese"), category = Category(id = 10, name = LocaledString("アーキテクチャ", "App Architecture")), intendedAudience = "intermediate", + videoUrl = "https://droidkaigi.jp/2019/#might_be_null", + slideUrl = "https://droidkaigi.jp/2019/#might_be_null", isInterpretationTarget = false, isFavorited = false, speakers = listOf(), diff --git a/feature/session/src/test/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionContentsStoreTest.kt b/feature/session/src/test/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionContentsStoreTest.kt index 0bdab1290..0c4f6fcf6 100644 --- a/feature/session/src/test/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionContentsStoreTest.kt +++ b/feature/session/src/test/java/io/github/droidkaigi/confsched2019/session/ui/store/SessionContentsStoreTest.kt @@ -7,7 +7,6 @@ import io.github.droidkaigi.confsched2019.dummySessionData import io.github.droidkaigi.confsched2019.ext.android.CoroutinePlugin import io.github.droidkaigi.confsched2019.ext.android.changedForever import io.github.droidkaigi.confsched2019.model.LoadingState -import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.model.SessionContents import io.kotlintest.shouldBe import io.mockk.MockKAnnotations diff --git a/feature/settings/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/settings/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..f237963dd 100644 --- a/feature/settings/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/settings/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.settings.test", appContext.getPackageName()); } } diff --git a/feature/sponsor/build.gradle b/feature/sponsor/build.gradle index cf0e069c2..6f38001f5 100644 --- a/feature/sponsor/build.gradle +++ b/feature/sponsor/build.gradle @@ -14,7 +14,6 @@ dependencies { implementation project(":model") implementation project(":data:repository") implementation project(':ext:android-extension') - implementation project(':ext:log') implementation Dep.Kotlin.stdlibJvm api Dep.Kotlin.coroutines diff --git a/feature/sponsor/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/sponsor/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..3a1c372bc 100644 --- a/feature/sponsor/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/sponsor/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.sponsor.test", appContext.getPackageName()); } } diff --git a/feature/system/build.gradle b/feature/system/build.gradle index d6fdbd989..6b873ffdf 100644 --- a/feature/system/build.gradle +++ b/feature/system/build.gradle @@ -19,6 +19,7 @@ dependencies { api Dep.Kotlin.coroutines implementation Dep.Kotlin.androidCoroutinesDispatcher implementation Dep.AndroidX.appCompat + implementation Dep.AndroidX.browser implementation Dep.Ktor.clientAndroid @@ -29,6 +30,8 @@ dependencies { kapt Dep.Dagger.compiler kapt Dep.Dagger.androidProcessor + implementation Dep.Timber.android + testImplementation Dep.Test.junit androidTestImplementation Dep.Test.testRunner testImplementation Dep.Test.slf4j diff --git a/feature/system/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/system/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..7b42d1cf2 100644 --- a/feature/system/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/system/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.system.test", appContext.getPackageName()); } } diff --git a/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ActivityActionCreator.kt b/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ActivityActionCreator.kt index 31c37b1f5..5d958eb16 100644 --- a/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ActivityActionCreator.kt +++ b/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ActivityActionCreator.kt @@ -2,14 +2,21 @@ package io.github.droidkaigi.confsched2019.system.actioncreator import android.content.Intent import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import io.github.droidkaigi.confsched2019.system.R import javax.inject.Inject class ActivityActionCreator @Inject constructor(val activity: FragmentActivity) { fun openUrl(url: String) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - activity.startActivity(intent) + val customTabsIntent = CustomTabsIntent.Builder() + .setShowTitle(true) + .enableUrlBarHiding() + .setToolbarColor(ContextCompat.getColor(activity, R.color.white)) + .build() + customTabsIntent.launchUrl(activity, Uri.parse(url)) } fun openVenueOnGoogleMap() { @@ -24,6 +31,13 @@ class ActivityActionCreator @Inject constructor(val activity: FragmentActivity) } } + fun shareUrl(url: String) { + val builder: ShareCompat.IntentBuilder = ShareCompat.IntentBuilder.from(activity) + builder.setText(url) + .setType("text/plain") + .startChooser() + } + companion object { const val LATITUDE_LOCATION = "35.696065" const val LONGITUDE_LOCATION = "139.690426" diff --git a/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ErrorHandler.kt b/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ErrorHandler.kt index 5ad6c2881..44f2b41e5 100644 --- a/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ErrorHandler.kt +++ b/feature/system/src/main/java/io/github/droidkaigi/confsched2019/system/actioncreator/ErrorHandler.kt @@ -8,10 +8,12 @@ import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher import io.github.droidkaigi.confsched2019.model.ErrorMessage import io.github.droidkaigi.confsched2019.system.BuildConfig import io.github.droidkaigi.confsched2019.system.R -import io.github.droidkaigi.confsched2019.util.logd -import io.github.droidkaigi.confsched2019.util.loge +import io.github.droidkaigi.confsched2019.timber.error import io.ktor.client.features.BadResponseStatusException import kotlinx.coroutines.CancellationException +import timber.log.Timber +import timber.log.debug +import timber.log.error import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -20,14 +22,14 @@ interface ErrorHandler { val dispatcher: Dispatcher fun onError(e: Throwable, msg: String? = null) { if (e.cause is CancellationException) { - logd(e = e) { + Timber.debug(e) { "coroutine canceled" } return } when (e) { is CancellationException -> - logd(e = e) { + Timber.debug(e) { "coroutine canceled" } is UnknownHostException, @@ -37,17 +39,17 @@ interface ErrorHandler { is FirebaseApiNotAvailableException, is FirebaseNetworkException -> { val message = ErrorMessage.of(R.string.system_error_network, e) - loge(e = e) + Timber.error(e) dispatcher.launchAndDispatch(Action.Error(message)) } is BadResponseStatusException -> { val message = ErrorMessage.of(R.string.system_error_server, e) - loge(e = e) + Timber.error(e) dispatcher.launchAndDispatch(Action.Error(message)) } else -> { val message = ErrorMessage.of(msg ?: e.message ?: e.javaClass.name ?: "", e) - loge(e = e) { + Timber.error(e) { (message as ErrorMessage.Message).message } if (BuildConfig.DEBUG) throw UnknownException(e) diff --git a/feature/user/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java b/feature/user/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java index 461cd5732..b84d79025 100644 --- a/feature/user/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java +++ b/feature/user/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.java @@ -20,6 +20,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("io.github.droidkaigi.confsched2019.ui.test", appContext.getPackageName()); + assertEquals("io.github.droidkaigi.confsched2019.test", appContext.getPackageName()); } } diff --git a/frontend/android/build.gradle b/frontend/android/build.gradle index bc655515f..e4e31f8f5 100644 --- a/frontend/android/build.gradle +++ b/frontend/android/build.gradle @@ -25,6 +25,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true + resConfigs "ja" // For Android Studio Gradle sync applicationIdSuffix ".debug" } @@ -41,8 +42,8 @@ android { lintOptions { lintConfig file("${project.projectDir}/lint.xml") - xmlReport System.getenv("CI") == "true" - htmlReport System.getenv("CI") != "true" + xmlReport isCi + htmlReport !isCi xmlOutput file("lint-results.xml") htmlOutput file("lint-results.html") @@ -76,6 +77,8 @@ dependencies { implementation Dep.Kotlin.stdlibJvm api Dep.Kotlin.coroutines + implementation Dep.Kotlin.androidCoroutinesDispatcher + implementation Dep.OkHttp.okio implementation Dep.Firebase.fireStore @@ -107,6 +110,8 @@ dependencies { compileOnly Dep.Dagger.assistedInjectAnnotations kapt Dep.Dagger.assistedInjectProcessor + implementation Dep.Timber.android + testImplementation Dep.Test.junit androidTestImplementation Dep.Test.testRunner diff --git a/frontend/android/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.kt b/frontend/android/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.kt index fa966205b..2291937d9 100644 --- a/frontend/android/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.kt +++ b/frontend/android/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/ExampleInstrumentedTest.kt @@ -16,6 +16,6 @@ class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getTargetContext() - assertEquals("io.github.droidkaigi.confsched2019.ui", appContext.packageName) + assertEquals("io.github.droidkaigi.confsched2019.debug", appContext.packageName) } } diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/App.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/App.kt index 1d83909dd..95d20a66e 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/App.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/App.kt @@ -15,7 +15,8 @@ import io.github.droidkaigi.confsched2019.di.createAppComponent import io.github.droidkaigi.confsched2019.ext.android.changedForever import io.github.droidkaigi.confsched2019.system.actioncreator.SystemActionCreator import io.github.droidkaigi.confsched2019.system.store.SystemStore -import io.github.droidkaigi.confsched2019.util.logd +import timber.log.Timber +import timber.log.debug import javax.inject.Inject open class App : DaggerApplication() { @@ -38,7 +39,7 @@ open class App : DaggerApplication() { // fetch font for cache ResourcesCompat.getFont(this, R.font.lekton, object : ResourcesCompat.FontCallback() { override fun onFontRetrievalFailed(reason: Int) { - logd { "onFontRetrievalFailed$reason" } + Timber.debug { "onFontRetrievalFailed$reason" } } override fun onFontRetrieved(typeface: Typeface) { diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/DebugApp.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/DebugApp.kt index a2f5696ad..e184c29a2 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/DebugApp.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/DebugApp.kt @@ -1,10 +1,9 @@ package io.github.droidkaigi.confsched2019 -import android.util.Log import com.facebook.stetho.Stetho import com.squareup.leakcanary.LeakCanary -import io.github.droidkaigi.confsched2019.util.LogLevel -import io.github.droidkaigi.confsched2019.util.logHandler +import timber.log.LogcatTree +import timber.log.Timber class DebugApp : App() { override fun onCreate() { @@ -26,21 +25,6 @@ class DebugApp : App() { } private fun setupLogHandler() { - logHandler = { logLevel: LogLevel, tag: String, e: Throwable?, logHandler: () -> String -> - val message = if (e != null) { - logHandler() + Log.getStackTraceString(e) - } else { - logHandler() - } - Log.println( - when (logLevel) { - LogLevel.DEBUG -> Log.DEBUG - LogLevel.WARN -> Log.WARN - LogLevel.ERROR -> Log.ERROR - }, - tag, - message - ) - } + Timber.plant(LogcatTree("droidkaigi")) } } diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/AppComponent.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/AppComponent.kt index 31ca8520e..5d6a7951f 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/AppComponent.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/AppComponent.kt @@ -16,7 +16,7 @@ import javax.inject.Singleton MainActivityModule.MainActivityBuilder::class, DbComponentModule::class, RepositoryComponentModule::class, - FireStoreComponentModule::class, + FirestoreComponentModule::class, ApiComponentModule::class ] ) diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FireStoreComponentModule.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FirestoreComponentModule.kt similarity index 59% rename from frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FireStoreComponentModule.kt rename to frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FirestoreComponentModule.kt index 521ec408a..e50454145 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FireStoreComponentModule.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/FirestoreComponentModule.kt @@ -2,17 +2,17 @@ package io.github.droidkaigi.confsched2019.di import dagger.Module import dagger.Provides -import io.github.droidkaigi.confsched2019.data.firestore.FireStore -import io.github.droidkaigi.confsched2019.data.repository.FireStoreComponent +import io.github.droidkaigi.confsched2019.data.firestore.Firestore +import io.github.droidkaigi.confsched2019.data.repository.FirestoreComponent import io.github.droidkaigi.confsched2019.ext.android.Dispatchers import javax.inject.Singleton @Module -object FireStoreComponentModule { - @JvmStatic @Provides @Singleton fun provideRepository(): FireStore { - return FireStoreComponent.builder() +object FirestoreComponentModule { + @JvmStatic @Provides @Singleton fun provideRepository(): Firestore { + return FirestoreComponent.builder() .coroutineContext(Dispatchers.IO) .build() - .fireStore() + .firestore() } } diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/RepositoryComponentModule.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/RepositoryComponentModule.kt index 4cb50c516..0fe749d71 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/RepositoryComponentModule.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/RepositoryComponentModule.kt @@ -6,7 +6,7 @@ import io.github.droidkaigi.confsched2019.data.api.DroidKaigiApi import io.github.droidkaigi.confsched2019.data.api.GoogleFormApi import io.github.droidkaigi.confsched2019.data.db.SessionDatabase import io.github.droidkaigi.confsched2019.data.db.SponsorDatabase -import io.github.droidkaigi.confsched2019.data.firestore.FireStore +import io.github.droidkaigi.confsched2019.data.firestore.Firestore import io.github.droidkaigi.confsched2019.data.repository.RepositoryComponent import io.github.droidkaigi.confsched2019.data.repository.SessionRepository import io.github.droidkaigi.confsched2019.data.repository.SponsorRepository @@ -19,14 +19,14 @@ object RepositoryComponentModule { googleFormApi: GoogleFormApi, database: SessionDatabase, sponsorDatabase: SponsorDatabase, - fireStore: FireStore + firestore: Firestore ): SessionRepository { return RepositoryComponent.builder() .droidKaigiApi(droidKaigiApi) .googleFormApi(googleFormApi) .database(database) .sponsorDatabase(sponsorDatabase) - .fireStore(fireStore) + .firestore(firestore) .build() .sessionRepository() } @@ -36,14 +36,14 @@ object RepositoryComponentModule { googleFormApi: GoogleFormApi, database: SessionDatabase, sponsorDatabase: SponsorDatabase, - fireStore: FireStore + firestore: Firestore ): SponsorRepository { return RepositoryComponent.builder() .droidKaigiApi(droidKaigiApi) .googleFormApi(googleFormApi) .database(database) .sponsorDatabase(sponsorDatabase) - .fireStore(fireStore) + .firestore(firestore) .build() .sponsorRepository() } diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt index ff113f66a..c78584a2e 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt @@ -3,10 +3,10 @@ package io.github.droidkaigi.confsched2019.ui import android.graphics.PorterDuff import android.os.Build import android.os.Bundle -import android.view.Menu import android.view.MenuItem import android.view.View import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil @@ -130,15 +130,18 @@ class MainActivity : DaggerAppCompatActivity() { } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.menu_toolbar, menu) - return true - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return NavigationUI.onNavDestinationSelected(item, navController) || super.onOptionsItemSelected(item) } + + override fun onBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + return + } + super.onBackPressed() + } } @Module diff --git a/frontend/android/src/main/res/drawable/bg_navigation_footer.xml b/frontend/android/src/main/res/drawable/bg_navigation_footer.xml new file mode 100644 index 000000000..8971a41ca --- /dev/null +++ b/frontend/android/src/main/res/drawable/bg_navigation_footer.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/frontend/android/src/main/res/drawable/ic_settings_outline_black_24px.xml b/frontend/android/src/main/res/drawable/ic_settings_outline_black_24dp.xml similarity index 100% rename from frontend/android/src/main/res/drawable/ic_settings_outline_black_24px.xml rename to frontend/android/src/main/res/drawable/ic_settings_outline_black_24dp.xml diff --git a/frontend/android/src/main/res/layout/activity_main.xml b/frontend/android/src/main/res/layout/activity_main.xml index f7672759d..07403b2ae 100644 --- a/frontend/android/src/main/res/layout/activity_main.xml +++ b/frontend/android/src/main/res/layout/activity_main.xml @@ -74,6 +74,9 @@ app:headerLayout="@layout/layout_navigation_header" app:menu="@menu/menu_nav_drawer" > + + + diff --git a/frontend/android/src/main/res/layout/layout_navigation_footer.xml b/frontend/android/src/main/res/layout/layout_navigation_footer.xml new file mode 100644 index 000000000..062183f3f --- /dev/null +++ b/frontend/android/src/main/res/layout/layout_navigation_footer.xml @@ -0,0 +1,48 @@ + + + + + + + diff --git a/frontend/android/src/main/res/menu/menu_nav_drawer.xml b/frontend/android/src/main/res/menu/menu_nav_drawer.xml index 56cfccac0..80acd0410 100644 --- a/frontend/android/src/main/res/menu/menu_nav_drawer.xml +++ b/frontend/android/src/main/res/menu/menu_nav_drawer.xml @@ -42,18 +42,17 @@ /> - - + + diff --git a/frontend/android/src/main/res/values-ja/strings.xml b/frontend/android/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..525128fb3 --- /dev/null +++ b/frontend/android/src/main/res/values-ja/strings.xml @@ -0,0 +1,3 @@ + + 全体アンケートに\nご協力ください + diff --git a/frontend/android/src/main/res/values/strings.xml b/frontend/android/src/main/res/values/strings.xml index 2df81721d..589f26d07 100644 --- a/frontend/android/src/main/res/values/strings.xml +++ b/frontend/android/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ DroidKaigi 2019 logo image + Please fill out the entire survey diff --git a/frontendcomponent/androidcomponent/build.gradle b/frontendcomponent/androidcomponent/build.gradle index 388441ba7..945a00a07 100644 --- a/frontendcomponent/androidcomponent/build.gradle +++ b/frontendcomponent/androidcomponent/build.gradle @@ -9,6 +9,7 @@ apply from: rootProject.file('gradle/android.gradle') dependencies { api project(":model") api Dep.AndroidX.design + api Dep.AndroidX.coreKtx implementation Dep.Kotlin.stdlibJvm api Dep.Dagger.androidSupport @@ -31,6 +32,7 @@ dependencies { androidTestImplementation Dep.Test.espressoCore } repositories { + if (!isCi) { maven { url "https://maven-central-asia.storage-download.googleapis.com/repos/central/data/" } } mavenCentral() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { diff --git a/frontendcomponent/androidcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java b/frontendcomponent/androidcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java deleted file mode 100644 index f8a05565d..000000000 --- a/frontendcomponent/androidcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget.component; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.component.test", appContext.getPackageName()); - } -} diff --git a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/action/Action.kt b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/action/Action.kt index fce9c1590..718c0fdc6 100644 --- a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/action/Action.kt +++ b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/action/Action.kt @@ -52,8 +52,8 @@ sealed class Action { object UserRegistered : Action() - class AnnouncementLoadingStateChanged(val loadingState: LoadingState) : Action() - class AnnouncementLoaded(val announcements: List) : Action() + data class AnnouncementLoadingStateChanged(val loadingState: LoadingState) : Action() + data class AnnouncementLoaded(val announcements: List) : Action() data class SponsorLoadingStateChanged(val loadingState: LoadingState) : Action() data class SponsorLoaded(val sponsors: List) : Action() diff --git a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/FilterChip.kt b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/FilterChip.kt new file mode 100644 index 000000000..f5156e88a --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/FilterChip.kt @@ -0,0 +1,365 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.droidkaigi.confsched2019.widget + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.Paint.ANTI_ALIAS_FLAG +import android.graphics.Paint.Style.STROKE +import android.graphics.drawable.Drawable +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.M +import android.text.Layout.Alignment.ALIGN_NORMAL +import android.text.StaticLayout +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import android.view.animation.AnimationUtils +import android.widget.Checkable +import androidx.annotation.ColorInt +import androidx.core.animation.doOnEnd +import androidx.core.content.res.getColorOrThrow +import androidx.core.content.res.getDimensionOrThrow +import androidx.core.content.res.getDimensionPixelSizeOrThrow +import androidx.core.content.res.getDrawableOrThrow +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.withScale +import androidx.core.graphics.withTranslation +import com.google.android.material.math.MathUtils.lerp +import io.github.droidkaigi.confsched2019.widget.component.R + +/** + * A custom view for displaying filters. Allows a custom presentation of the tag color and selection + * state. + */ +class FilterChip @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), Checkable { + + var color: Int = 0 + set(value) { + if (field != value) { + field = value + dotPaint.color = value + postInvalidateOnAnimation() + } + } + + var selectedTextColor: Int? = null + + var text: CharSequence = "" + set(value) { + field = value + updateContentDescription() + requestLayout() + } + + var showIcons: Boolean = true + set(value) { + if (field != value) { + field = value + requestLayout() + } + } + + var onCheckedChangeListener: OnCheckedChangeListener? = null + + private var progress = 0f + set(value) { + if (field != value) { + val prevChecked = isChecked + field = value + val newChecked = isChecked + postInvalidateOnAnimation() + if (value == 0f || value == 1f) { + updateContentDescription() + } + if (newChecked != prevChecked) { + onCheckedChangeListener?.onCheckedChanged(this, newChecked) + } + } + } + + private val padding: Int + + private val outlinePaint: Paint + + private val textPaint: TextPaint + + private val dotPaint: Paint + + private val clear: Drawable + + private val touchFeedback: Drawable + + private lateinit var textLayout: StaticLayout + + private var progressAnimator: ValueAnimator? = null + + private val interp = + AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in) + + @ColorInt private val defaultTextColor: Int + + init { + val a = context.obtainStyledAttributes( + attrs, + R.styleable.FilterChip, + R.attr.filterChipStyle, + 0 + ) + outlinePaint = Paint(ANTI_ALIAS_FLAG).apply { + color = a.getColorOrThrow(R.styleable.FilterChip_strokeColor) + strokeWidth = a.getDimensionOrThrow(R.styleable.FilterChip_strokeWidth) + style = STROKE + } + defaultTextColor = a.getColorOrThrow(R.styleable.FilterChip_android_textColor) + selectedTextColor = a.getColor(R.styleable.FilterChip_selectedTextColor, 0) + textPaint = TextPaint(ANTI_ALIAS_FLAG).apply { + color = defaultTextColor + textSize = a.getDimensionOrThrow(R.styleable.FilterChip_android_textSize) + } + dotPaint = Paint(ANTI_ALIAS_FLAG) + color = a.getColor(R.styleable.FilterChip_android_color, 0) + clear = a.getDrawableOrThrow(R.styleable.FilterChip_clearIcon).apply { + setBounds( + -intrinsicWidth / 2, -intrinsicHeight / 2, intrinsicWidth / 2, intrinsicHeight / 2 + ) + } + touchFeedback = a.getDrawableOrThrow(R.styleable.FilterChip_foreground).apply { + callback = this@FilterChip + } + padding = a.getDimensionPixelSizeOrThrow(R.styleable.FilterChip_android_padding) + isChecked = a.getBoolean(R.styleable.FilterChip_android_checked, false) + showIcons = a.getBoolean(R.styleable.FilterChip_showIcons, true) + a.recycle() + clipToOutline = true + setOnClickListener { toggleWithAnimation() } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val nonTextWidth = (4 * padding) + + (2 * outlinePaint.strokeWidth).toInt() + + if (showIcons) clear.intrinsicWidth else 0 + val availableTextWidth = when (MeasureSpec.getMode(widthMeasureSpec)) { + MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec) - nonTextWidth + MeasureSpec.AT_MOST -> MeasureSpec.getSize(widthMeasureSpec) - nonTextWidth + MeasureSpec.UNSPECIFIED -> Int.MAX_VALUE + else -> Int.MAX_VALUE + } + createLayout(availableTextWidth) + val w = nonTextWidth + textLayout.textWidth() + val h = padding + textLayout.height + padding + setMeasuredDimension(w, h) + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, w, h, h / 2f) + } + } + touchFeedback.setBounds(0, 0, w, h) + } + + override fun onDraw(canvas: Canvas) { + val strokeWidth = outlinePaint.strokeWidth + val iconRadius = clear.intrinsicWidth / 2f + val halfStroke = strokeWidth / 2f + val rounding = (height - strokeWidth) / 2f + + // Outline + if (progress < 1f) { + canvas.drawRoundRect( + halfStroke, + halfStroke, + width - halfStroke, + height - halfStroke, + rounding, + rounding, + outlinePaint + ) + } + + // Tag color dot/background + if (showIcons) { + // Draws beyond bounds and relies on clipToOutline to enforce pill shape + val dotRadius = lerp( + strokeWidth + iconRadius, + width.toFloat(), + progress + ) + canvas.drawCircle(strokeWidth + padding + iconRadius, height / 2f, dotRadius, dotPaint) + } else { + canvas.drawRoundRect( + halfStroke, + halfStroke, + width - halfStroke, + height - halfStroke, + rounding, + rounding, + dotPaint + ) + } + + // Text + val textX = if (showIcons) { + lerp( + strokeWidth + padding + clear.intrinsicWidth + padding, + strokeWidth + padding * 2f, + progress + ) + } else { + strokeWidth + padding * 2f + } + val selectedColor = selectedTextColor + textPaint.color = if (selectedColor != null && selectedColor != 0 && progress > 0) { + ColorUtils.blendARGB(defaultTextColor, selectedColor, progress) + } else { + defaultTextColor + } + canvas.withTranslation( + x = textX, + y = (height - textLayout.height) / 2f + ) { + textLayout.draw(canvas) + } + + // Clear icon + if (showIcons && progress > 0f) { + canvas.withTranslation( + x = width - strokeWidth - padding - iconRadius, + y = height / 2f + ) { + canvas.withScale(progress, progress) { + clear.draw(canvas) + } + } + } + + // Touch feedback + touchFeedback.draw(canvas) + } + + /** + * Starts the animation to enable/disable a filter and invokes a function when done. + */ + fun animateCheckedAndInvoke(checked: Boolean, onEnd: (() -> Unit)?) { + val newProgress = if (checked) 1f else 0f + if (newProgress != progress) { + progressAnimator?.cancel() + progressAnimator = ValueAnimator.ofFloat(progress, newProgress).apply { + addUpdateListener { + progress = it.animatedValue as Float + } + doOnEnd { + progress = newProgress + onEnd?.invoke() + } + interpolator = interp + duration = if (checked) SELECTING_DURATION else DESELECTING_DURATION + start() + } + } + } + + override fun isChecked() = progress == 1f + + override fun toggle() { + progress = if (isChecked) 0f else 1f + } + + override fun setChecked(checked: Boolean) { + progress = if (checked) 1f else 0f + } + + fun toggleWithAnimation() { + setCheckedWithAnimation(!isChecked) + } + + fun setCheckedWithAnimation(checked: Boolean) { + animateCheckedAndInvoke(checked, null) + } + + override fun verifyDrawable(who: Drawable): Boolean { + return super.verifyDrawable(who) || who == touchFeedback + } + + override fun drawableStateChanged() { + super.drawableStateChanged() + touchFeedback.state = drawableState + } + + override fun jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState() + touchFeedback.jumpToCurrentState() + } + + override fun drawableHotspotChanged(x: Float, y: Float) { + super.drawableHotspotChanged(x, y) + touchFeedback.setHotspot(x, y) + } + + private fun createLayout(textWidth: Int) { + textLayout = if (SDK_INT >= M) { + StaticLayout.Builder.obtain(text, 0, text.length, textPaint, textWidth).build() + } else { + @Suppress("DEPRECATION") + StaticLayout(text, textPaint, textWidth, ALIGN_NORMAL, 1f, 0f, true) + } + } + + private fun updateContentDescription() { + val desc = if (isChecked) { + R.string.description_filter_applied + } else { + R.string.description_filter_not_applied + } + contentDescription = resources.getString(desc, text) + } + + /** + * Calculated the widest line in a [StaticLayout]. + */ + private fun StaticLayout.textWidth(): Int { + var width = 0f + for (i in 0 until lineCount) { + width = width.coerceAtLeast(getLineWidth(i)) + } + return width.toInt() + } + + interface OnCheckedChangeListener { + fun onCheckedChanged(chip: FilterChip, isChecked: Boolean) + } + + companion object { + private const val SELECTING_DURATION = 350L + private const val DESELECTING_DURATION = 200L + } +} + +fun FilterChip.onCheckedChanged(action: (FilterChip, Boolean) -> Unit) { + onCheckedChangeListener = object : FilterChip.OnCheckedChangeListener { + override fun onCheckedChanged(chip: FilterChip, isChecked: Boolean) { + action(chip, isChecked) + } + } +} diff --git a/frontendcomponent/androidcomponent/src/main/res/drawable-hdpi/ic_droidkaigi.png b/frontendcomponent/androidcomponent/src/main/res/drawable-hdpi/ic_droidkaigi.png new file mode 100644 index 000000000..f35bfc4de Binary files /dev/null and b/frontendcomponent/androidcomponent/src/main/res/drawable-hdpi/ic_droidkaigi.png differ diff --git a/frontendcomponent/androidcomponent/src/main/res/drawable-xhdpi/ic_droidkaigi.png b/frontendcomponent/androidcomponent/src/main/res/drawable-xhdpi/ic_droidkaigi.png new file mode 100644 index 000000000..55d3b61ad Binary files /dev/null and b/frontendcomponent/androidcomponent/src/main/res/drawable-xhdpi/ic_droidkaigi.png differ diff --git a/frontendcomponent/androidcomponent/src/main/res/drawable-xxhdpi/ic_droidkaigi.png b/frontendcomponent/androidcomponent/src/main/res/drawable-xxhdpi/ic_droidkaigi.png new file mode 100644 index 000000000..d8e1c81d0 Binary files /dev/null and b/frontendcomponent/androidcomponent/src/main/res/drawable-xxhdpi/ic_droidkaigi.png differ diff --git a/frontendcomponent/androidcomponent/src/main/res/drawable-xxxhdpi/ic_droidkaigi.png b/frontendcomponent/androidcomponent/src/main/res/drawable-xxxhdpi/ic_droidkaigi.png new file mode 100644 index 000000000..422ef5683 Binary files /dev/null and b/frontendcomponent/androidcomponent/src/main/res/drawable-xxxhdpi/ic_droidkaigi.png differ diff --git a/frontendcomponent/androidcomponent/src/main/res/drawable/tag_clear.xml b/frontendcomponent/androidcomponent/src/main/res/drawable/tag_clear.xml new file mode 100644 index 000000000..71afac4c5 --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/res/drawable/tag_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/frontendcomponent/androidcomponent/src/main/res/values-ja/strings.xml b/frontendcomponent/androidcomponent/src/main/res/values-ja/strings.xml index 9d80623bb..d8f13296b 100644 --- a/frontendcomponent/androidcomponent/src/main/res/values-ja/strings.xml +++ b/frontendcomponent/androidcomponent/src/main/res/values-ja/strings.xml @@ -6,4 +6,6 @@ Droidkaigiとは スポンサーページ 全体アンケート + フィルター%1$sを適用 + フィルター%1$sを解除 diff --git a/frontendcomponent/androidcomponent/src/main/res/values/attrs.xml b/frontendcomponent/androidcomponent/src/main/res/values/attrs.xml new file mode 100644 index 000000000..65cae3e7d --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/res/values/attrs.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontendcomponent/androidcomponent/src/main/res/values/strings.xml b/frontendcomponent/androidcomponent/src/main/res/values/strings.xml index 882ab46b4..dc1b96338 100644 --- a/frontendcomponent/androidcomponent/src/main/res/values/strings.xml +++ b/frontendcomponent/androidcomponent/src/main/res/values/strings.xml @@ -19,4 +19,7 @@ Search Setting + + %1$s filter applied + %1$s filter not applied diff --git a/frontendcomponent/androidcomponent/src/main/res/values/styles.xml b/frontendcomponent/androidcomponent/src/main/res/values/styles.xml index 045e125f3..dfd3ed4f0 100644 --- a/frontendcomponent/androidcomponent/src/main/res/values/styles.xml +++ b/frontendcomponent/androidcomponent/src/main/res/values/styles.xml @@ -1,3 +1,14 @@ + + diff --git a/frontendcomponent/androidcomponent/src/main/res/values/themes.xml b/frontendcomponent/androidcomponent/src/main/res/values/themes.xml index 7b0286375..1f1e5098c 100644 --- a/frontendcomponent/androidcomponent/src/main/res/values/themes.xml +++ b/frontendcomponent/androidcomponent/src/main/res/values/themes.xml @@ -13,6 +13,7 @@ true @android:color/transparent @style/SearchViewStyle + @style/Widget.App.FilterChip @style/TextAppearance.App.Headline5 diff --git a/frontendcomponent/androidtestcomponent/build.gradle b/frontendcomponent/androidtestcomponent/build.gradle index 0640d3aa6..b905a6bd3 100644 --- a/frontendcomponent/androidtestcomponent/build.gradle +++ b/frontendcomponent/androidtestcomponent/build.gradle @@ -23,6 +23,7 @@ dependencies { } } repositories { + if (!isCi) { maven { url "https://maven-central-asia.storage-download.googleapis.com/repos/central/data/" } } mavenCentral() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { diff --git a/frontendcomponent/androidtestcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java b/frontendcomponent/androidtestcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java deleted file mode 100644 index f8a05565d..000000000 --- a/frontendcomponent/androidtestcomponent/src/androidTest/java/io/github/droidkaigi/confsched2019/widget/component/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.droidkaigi.confsched2019.widget.component; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("io.github.droidkaigi.confsched2019.ui.component.test", appContext.getPackageName()); - } -} diff --git a/model/src/commonMain/kotlin/Filters.kt b/model/src/commonMain/kotlin/Filters.kt index 55f4c6cf2..b4a9b371f 100644 --- a/model/src/commonMain/kotlin/Filters.kt +++ b/model/src/commonMain/kotlin/Filters.kt @@ -23,4 +23,8 @@ data class Filters( } return roomFilterOk && categoryFilterOk && langFilterOk } + + fun isFiltered(): Boolean { + return rooms.isNotEmpty() || categories.isNotEmpty() || langs.isNotEmpty() + } } diff --git a/model/src/commonMain/kotlin/LocaleMessage.kt b/model/src/commonMain/kotlin/LocaleMessage.kt deleted file mode 100644 index 6c30e43c8..000000000 --- a/model/src/commonMain/kotlin/LocaleMessage.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.droidkaigi.confsched2019.model - -class LocaleMessage(val enMessage: String, val jaMessage: String) - -fun LocaleMessage.get() = if (defaultLang() != Lang.JA) enMessage else jaMessage diff --git a/model/src/commonMain/kotlin/Session.kt b/model/src/commonMain/kotlin/Session.kt index e69a4e4e0..b3c365190 100644 --- a/model/src/commonMain/kotlin/Session.kt +++ b/model/src/commonMain/kotlin/Session.kt @@ -8,7 +8,8 @@ sealed class Session( open val dayNumber: Int, open val startTime: DateTime, open val endTime: DateTime, - open val room: Room + open val room: Room, + open val isFavorited: Boolean ) { data class SpeechSession( override val id: String, @@ -22,11 +23,16 @@ sealed class Session( val language: LocaledString, val category: Category, val intendedAudience: String?, + val videoUrl: String?, + val slideUrl: String?, val isInterpretationTarget: Boolean, - val isFavorited: Boolean, + override val isFavorited: Boolean, val speakers: List, - val message: SessionMessage? - ) : Session(id, dayNumber, startTime, endTime, room) + val message: LocaledString? + ) : Session(id, dayNumber, startTime, endTime, room, isFavorited) { + val hasVideo: Boolean = videoUrl.isNullOrEmpty().not() + val hasSlide: Boolean = slideUrl.isNullOrEmpty().not() + } data class ServiceSession( override val id: String, @@ -35,8 +41,9 @@ sealed class Session( override val endTime: DateTime, val title: String, override val room: Room, - val sessionType: SessionType - ) : Session(id, dayNumber, startTime, endTime, room) + val sessionType: SessionType, + override val isFavorited: Boolean + ) : Session(id, dayNumber, startTime, endTime, room, isFavorited) val startDayText by lazy { startTime.format("yyyy.M.d") } @@ -53,9 +60,9 @@ sealed class Session( append("日") } append(" ") - append(startTime.format("hh:mm")) + append(startTime.format("HH:mm")) append(" - ") - append(endTime.format("hh:mm")) + append(endTime.format("HH:mm")) } fun summary(lang: Lang) = buildString { diff --git a/model/src/commonMain/kotlin/SessionMessage.kt b/model/src/commonMain/kotlin/SessionMessage.kt deleted file mode 100644 index b9bd1be8c..000000000 --- a/model/src/commonMain/kotlin/SessionMessage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.droidkaigi.confsched2019.model - -data class SessionMessage( - val jaMessage: String, - val enMessage: String -) { - - fun getBodyByLang(lang: Lang): String = if (lang == Lang.JA) { - jaMessage - } else { - enMessage - } -} diff --git a/model/src/commonMain/kotlin/SessionType.kt b/model/src/commonMain/kotlin/SessionType.kt index 89bd3c741..e59fbebb9 100644 --- a/model/src/commonMain/kotlin/SessionType.kt +++ b/model/src/commonMain/kotlin/SessionType.kt @@ -1,15 +1,15 @@ package io.github.droidkaigi.confsched2019.model -enum class SessionType(val id: String) { - Normal("normal"), - WelcomeTalk("welcome_talk"), - Reserved("reserved"), - Codelabs("codelabs"), - FiresideChat("fireside_chat"), - Lunch("lunch"), - Break("break"), - AfterParty("after_party"), - Unknown("unknown"); +enum class SessionType(val id: String, val isFavoritable: Boolean) { + Normal("normal", false), + WelcomeTalk("welcome_talk", false), + Reserved("reserved", false), + Codelabs("codelabs", true), + FiresideChat("fireside_chat", false), + Lunch("lunch", false), + Break("break", false), + AfterParty("after_party", false), + Unknown("unknown", false); companion object { fun of(id: String?) = values().find { it.id == id } ?: Unknown diff --git a/model/src/jvmTest/kotlin/io/github/droidkaigi/confsched2019/model/FiltersTest.kt b/model/src/jvmTest/kotlin/io/github/droidkaigi/confsched2019/model/FiltersTest.kt index 907478803..cd833d95c 100644 --- a/model/src/jvmTest/kotlin/io/github/droidkaigi/confsched2019/model/FiltersTest.kt +++ b/model/src/jvmTest/kotlin/io/github/droidkaigi/confsched2019/model/FiltersTest.kt @@ -11,7 +11,7 @@ class FiltersTest { assertTrue { Filters().isPass(mockk()) } } - @Test fun isPass_WhenSpecialSession() { + @Test fun isPass_WhenServiceSession() { assertTrue { Filters().isPass(mockk()) } } diff --git a/scripts/checkout_pr_branch b/scripts/checkout_pr_branch new file mode 100755 index 000000000..8be069b2b --- /dev/null +++ b/scripts/checkout_pr_branch @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +user_name="${CIRCLE_PROJECT_USERNAME:-DroidKaigi}" +repo_name="${CIRCLE_PROJECT_REPONAME:-conference-app-2019}" + +get_pr() { + local -r pr_number="$1" + + # DANGER_GITHUB_API_TOKEN, GITHUB_API_TOKEN, GITHUB_ACCESS_TOKEN are supported + local -r gh_token="${DANGER_GITHUB_API_TOKEN:-${GITHUB_API_TOKEN:-$GITHUB_ACCESS_TOKEN}}" + + curl -H "Authorization: token $gh_token" \ + "https://api.github.com/repos/$user_name/$repo_name/pulls/$pr_number" +} + +get_pr_meta() { + cat - | jq -r '.head | .repo.full_name, .ref' +} + +sync() { + local -r remote_name="$1" + local full_name= ref= + + read full_name + read ref + + git remote add "$remote_name" "git@github.com:$full_name.git" || : + git fetch "$remote_name" "$ref" + git checkout -b "${remote_name}_${ref}" "$remote_name/$ref" +} + +sync_forked_pr() { + local -r pr_number="$1" + + get_pr "$pr_number" | get_pr_meta | sync "pr_$pr_number" +} + +sync_forked_pr "$1" \ No newline at end of file diff --git a/scripts/danger/Dangerfile.assertions b/scripts/danger/Dangerfile.assertions index f2854e2e5..8c3206480 100644 --- a/scripts/danger/Dangerfile.assertions +++ b/scripts/danger/Dangerfile.assertions @@ -24,9 +24,9 @@ begin if File.exists?(ENV.fetch('MERGED_JUNIT_RESULT_FILE')) # junit report junit.parse ENV.fetch('MERGED_JUNIT_RESULT_FILE') - fail("Failed tests found") unless junit.failures.empty? + fail("Failed tests found. Please fix test failures.") unless junit.failures.empty? else - fail("Test results not found") + fail("The previous test step failed. Please check the CI log.") end end @@ -44,6 +44,6 @@ end # LGTM begin if status_report[:errors].length.zero? && status_report[:warnings].length.zero? - markdown("ALL GREEN :100:") + markdown("Asserted successfully. :100:") end end diff --git a/scripts/fetch_droidkaigi_master b/scripts/fetch_droidkaigi_master new file mode 100755 index 000000000..c8a0eb3ee --- /dev/null +++ b/scripts/fetch_droidkaigi_master @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +user_name="${CIRCLE_PROJECT_USERNAME:-DroidKaigi}" +repo_name="${CIRCLE_PROJECT_REPONAME:-conference-app-2019}" + +sync_origin_master() { + git remote add "$user_name" "git@github.com:$user_name/$repo_name.git" + git fetch "$user_name" master + + cat<