diff --git a/.github/workflows/AndroidCIWithGmd.yaml b/.github/workflows/AndroidCIWithGmd.yaml index bae399f47..44f8141f6 100644 --- a/.github/workflows/AndroidCIWithGmd.yaml +++ b/.github/workflows/AndroidCIWithGmd.yaml @@ -27,7 +27,7 @@ jobs: - name: Run instrumented tests with GMD run: ./gradlew cleanManagedDevices --unused-only && ./gradlew ${{ matrix.device-config }}DemoDebugAndroidTest -Dorg.gradle.workers.max=1 - -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true --info + -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true - name: Upload test reports if: success() || failure() diff --git a/app-nia-catalog/build.gradle.kts b/app-nia-catalog/build.gradle.kts index 8232350d9..bf0695fd3 100644 --- a/app-nia-catalog/build.gradle.kts +++ b/app-nia-catalog/build.gradle.kts @@ -65,9 +65,8 @@ android { } dependencies { - implementation(project(":core:ui")) implementation(project(":core:designsystem")) - - implementation(libs.androidx.activity.compose) + implementation(project(":core:ui")) implementation(libs.accompanist.flowlayout) + implementation(libs.androidx.activity.compose) } diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 9af89d98d..fa8aeefb0 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -68,13 +68,13 @@ android { } dependencies { + implementation(libs.androidx.benchmark.macro) implementation(libs.androidx.test.core) implementation(libs.androidx.test.espresso.core) implementation(libs.androidx.test.ext) - implementation(libs.androidx.test.runner) implementation(libs.androidx.test.rules) + implementation(libs.androidx.test.runner) implementation(libs.androidx.test.uiautomator) - implementation(libs.androidx.benchmark.macro) } androidComponents { diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 0b929d4f7..281434b87 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -27,9 +27,9 @@ java { dependencies { compileOnly(libs.android.gradlePlugin) - compileOnly(libs.kotlin.gradlePlugin) - compileOnly(libs.firebase.performance.gradle) compileOnly(libs.firebase.crashlytics.gradle) + compileOnly(libs.firebase.performance.gradle) + compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.ksp.gradlePlugin) } diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index e42499769..8c573b854 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -24,10 +24,9 @@ android { } dependencies { - implementation(libs.kotlinx.coroutines.android) + implementation(platform(libs.firebase.bom)) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.core.ktx) - - implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) + implementation(libs.kotlinx.coroutines.android) } diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 5b468c43e..51dfb5393 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -25,24 +25,24 @@ android { testOptions { unitTests { isIncludeAndroidResources = true + isReturnDefaultValues = true } } } dependencies { + implementation(project(":core:analytics")) implementation(project(":core:common")) - implementation(project(":core:model")) implementation(project(":core:database")) implementation(project(":core:datastore")) + implementation(project(":core:model")) implementation(project(":core:network")) - implementation(project(":core:analytics")) - - testImplementation(project(":core:testing")) - testImplementation(project(":core:datastore-test")) - + implementation(project(":core:notifications")) implementation(libs.androidx.core.ktx) - - implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) -} \ No newline at end of file + + testImplementation(project(":core:datastore-test")) + testImplementation(project(":core:testing")) +} diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt index c16355d69..02c58d855 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt @@ -30,7 +30,9 @@ import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource +import com.google.samples.apps.nowinandroid.core.notifications.Notifier import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -46,6 +48,7 @@ class OfflineFirstNewsRepository @Inject constructor( private val newsResourceDao: NewsResourceDao, private val topicDao: TopicDao, private val network: NiaNetworkDataSource, + private val notifier: Notifier, ) : NewsRepository { override fun getNewsResources( @@ -69,6 +72,16 @@ class OfflineFirstNewsRepository @Inject constructor( }, modelDeleter = newsResourceDao::deleteNewsResources, modelUpdater = { changedIds -> + // TODO: Make this more efficient, there is no need to retrieve populated + // news resources when all that's needed are the ids + val existingNewsResourceIds = newsResourceDao.getNewsResources( + useFilterNewsIds = true, + filterNewsIds = changedIds.toSet(), + ) + .first() + .map { it.entity.id } + .toSet() + changedIds.chunked(SYNC_BATCH_SIZE).forEach { chunkedIds -> val networkNewsResources = network.getNewsResources(ids = chunkedIds) @@ -92,6 +105,20 @@ class OfflineFirstNewsRepository @Inject constructor( .flatten(), ) } + + val addedNewsResources = newsResourceDao.getNewsResources( + useFilterNewsIds = true, + filterNewsIds = changedIds.toSet(), + ) + .first() + .filter { !existingNewsResourceIds.contains(it.entity.id) } + .map(PopulatedNewsResource::asExternalModel) + + // TODO: Define business logic for notifications on first time sync. + // we probably do not want to send notifications on first install. + // We can easily check if the change list version is 0 and not send notifications + // if it is. + if (addedNewsResources.isNotEmpty()) notifier.onNewsAdded(addedNewsResources) }, ) } diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncStatusMonitor.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncManager.kt similarity index 94% rename from core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncStatusMonitor.kt rename to core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncManager.kt index 14823ed0e..d72fa27a6 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncStatusMonitor.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncManager.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow /** * Reports on if synchronization is in progress */ -interface SyncStatusMonitor { +interface SyncManager { val isSyncing: Flow + fun requestSync() } diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt index 6b424e69e..6cdbf67d0 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt @@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource +import com.google.samples.apps.nowinandroid.core.testing.notifications.TestNotifier import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -58,6 +59,8 @@ class OfflineFirstNewsRepositoryTest { private lateinit var network: TestNiaNetworkDataSource + private lateinit var notifier: TestNotifier + private lateinit var synchronizer: Synchronizer @get:Rule @@ -68,6 +71,7 @@ class OfflineFirstNewsRepositoryTest { newsResourceDao = TestNewsResourceDao() topicDao = TestTopicDao() network = TestNiaNetworkDataSource() + notifier = TestNotifier() synchronizer = TestSynchronizer( NiaPreferencesDataSource( tmpFolder.testUserPreferencesDataStore(testScope), @@ -78,6 +82,7 @@ class OfflineFirstNewsRepositoryTest { newsResourceDao = newsResourceDao, topicDao = topicDao, network = network, + notifier = notifier, ) } @@ -145,6 +150,12 @@ class OfflineFirstNewsRepositoryTest { expected = network.latestChangeListVersion(CollectionType.NewsResources), actual = synchronizer.getChangeListVersions().newsResourceVersion, ) + + // Notifier should have been called with new news resources + assertEquals( + expected = newsResourcesFromDb.map(NewsResource::id).sorted(), + actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(), + ) } @Test @@ -186,6 +197,13 @@ class OfflineFirstNewsRepositoryTest { expected = network.latestChangeListVersion(CollectionType.NewsResources), actual = synchronizer.getChangeListVersions().newsResourceVersion, ) + + // Notifier should have been called with news resources from network that are not + // deleted + assertEquals( + expected = (newsResourcesFromNetwork.map(NewsResource::id) - deletedItems).sorted(), + actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(), + ) } @Test @@ -225,6 +243,12 @@ class OfflineFirstNewsRepositoryTest { expected = changeList.last().changeListVersion, actual = synchronizer.getChangeListVersions().newsResourceVersion, ) + + // Notifier should have been called with only added news resources from network + assertEquals( + expected = newsResourcesFromNetwork.map(NewsResource::id).sorted(), + actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(), + ) } @Test diff --git a/core/datastore-test/build.gradle.kts b/core/datastore-test/build.gradle.kts index d8223c3f3..c7c423c25 100644 --- a/core/datastore-test/build.gradle.kts +++ b/core/datastore-test/build.gradle.kts @@ -24,8 +24,8 @@ android { dependencies { api(project(":core:datastore")) + api(libs.androidx.dataStore.core) + implementation(project(":core:common")) implementation(project(":core:testing")) - - api(libs.androidx.dataStore.core) } diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 8f3d7ece6..1787a5b8f 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -33,6 +33,11 @@ android { consumerProguardFiles("consumer-proguard-rules.pro") } namespace = "com.google.samples.apps.nowinandroid.core.datastore" + testOptions { + unitTests { + isReturnDefaultValues = true + } + } } // Setup protobuf configuration, generating lite Java and Kotlin classes @@ -57,12 +62,10 @@ protobuf { dependencies { implementation(project(":core:common")) implementation(project(":core:model")) - - testImplementation(project(":core:testing")) - testImplementation(project(":core:datastore-test")) - - implementation(libs.kotlinx.coroutines.android) - implementation(libs.androidx.dataStore.core) + implementation(libs.kotlinx.coroutines.android) implementation(libs.protobuf.kotlin.lite) + + testImplementation(project(":core:datastore-test")) + testImplementation(project(":core:testing")) } diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 1bcc9d65c..a40926383 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -30,16 +30,20 @@ android { } dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.coil.kt.compose) + lintPublish(project(":lint")) + api(libs.androidx.compose.foundation) api(libs.androidx.compose.foundation.layout) api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material3) - debugApi(libs.androidx.compose.ui.tooling) + api(libs.androidx.compose.runtime) api(libs.androidx.compose.ui.tooling.preview) api(libs.androidx.compose.ui.util) - api(libs.androidx.compose.runtime) - lintPublish(project(":lint")) + + debugApi(libs.androidx.compose.ui.tooling) + + implementation(libs.androidx.core.ktx) + implementation(libs.coil.kt.compose) + androidTestImplementation(project(":core:testing")) } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 8483d890c..0e3949aa3 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -24,15 +24,13 @@ android { } dependencies { - implementation(project(":core:data")) implementation(project(":core:model")) - - testImplementation(project(":core:testing")) - + implementation(libs.hilt.android) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.datetime) - implementation(libs.hilt.android) kapt(libs.hilt.compiler) + + testImplementation(project(":core:testing")) } \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 97a68b3a5..633e2573d 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -41,17 +41,14 @@ secrets { dependencies { implementation(project(":core:common")) implementation(project(":core:model")) - - testImplementation(project(":core:testing")) - + implementation(libs.coil.kt) + implementation(libs.coil.kt.svg) implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime) - + implementation(libs.kotlinx.serialization.json) implementation(libs.okhttp.logging) implementation(libs.retrofit.core) implementation(libs.retrofit.kotlin.serialization) - implementation(libs.coil.kt) - implementation(libs.coil.kt.svg) + testImplementation(project(":core:testing")) } diff --git a/core/notifications/.gitignore b/core/notifications/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/notifications/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/notifications/build.gradle.kts b/core/notifications/build.gradle.kts new file mode 100644 index 000000000..608e59a38 --- /dev/null +++ b/core/notifications/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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. + */ +plugins { + id("nowinandroid.android.library") + id("nowinandroid.android.library.compose") + id("nowinandroid.android.hilt") +} + +android { + namespace = "com.google.samples.apps.nowinandroid.core.notifications" +} + +dependencies { + implementation(project(":core:model")) + + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.core.ktx) + + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.cloud.messaging) +} diff --git a/core/notifications/src/demo/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt b/core/notifications/src/demo/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt new file mode 100644 index 000000000..9bb2b3fb9 --- /dev/null +++ b/core/notifications/src/demo/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.notifications + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class NotificationsModule { + @Binds + abstract fun bindNotifier( + notifier: NoOpNotifier, + ): Notifier +} diff --git a/core/notifications/src/main/AndroidManifest.xml b/core/notifications/src/main/AndroidManifest.xml new file mode 100644 index 000000000..31c889874 --- /dev/null +++ b/core/notifications/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt new file mode 100644 index 000000000..00d97fcb3 --- /dev/null +++ b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.notifications + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of [Notifier] that displays notifications in the system tray. + */ +@Singleton +class AndroidSystemNotifier @Inject constructor() : Notifier { + + override fun onNewsAdded(newsResources: List) { + // TODO, create notification and display to the user + } +} diff --git a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/NoOpNotifier.kt b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/NoOpNotifier.kt new file mode 100644 index 000000000..5a8141e91 --- /dev/null +++ b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/NoOpNotifier.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.notifications + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import javax.inject.Inject + +/** + * Implementation of [Notifier] which does nothing. Useful for tests and previews. + */ +class NoOpNotifier @Inject constructor() : Notifier { + override fun onNewsAdded(newsResources: List) = Unit +} diff --git a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/Notifier.kt b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/Notifier.kt new file mode 100644 index 000000000..3084dcb75 --- /dev/null +++ b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/Notifier.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.notifications + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource + +/** + * Interface for creating notifications in the app + */ +interface Notifier { + fun onNewsAdded(newsResources: List) +} diff --git a/core/notifications/src/main/res/values/strings.xml b/core/notifications/src/main/res/values/strings.xml new file mode 100644 index 000000000..e3fd73ff8 --- /dev/null +++ b/core/notifications/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + Now in Android + Sync + Background tasks for Now in Android + + diff --git a/core/notifications/src/prod/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt b/core/notifications/src/prod/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt new file mode 100644 index 000000000..0b4bd6bae --- /dev/null +++ b/core/notifications/src/prod/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.notifications + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class NotificationsModule { + @Binds + abstract fun bindNotifier( + notifier: AndroidSystemNotifier, + ): Notifier +} diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 5e0c3e409..2fea5beb3 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -24,23 +24,22 @@ android { } dependencies { - implementation(project(":core:common")) - implementation(project(":core:data")) - implementation(project(":core:domain")) - implementation(project(":core:model")) - - implementation(libs.kotlinx.datetime) - - api(libs.junit4) + api(libs.androidx.compose.ui.test) api(libs.androidx.test.core) - api(libs.kotlinx.coroutines.test) - api(libs.turbine) - api(libs.androidx.test.espresso.core) - api(libs.androidx.test.runner) api(libs.androidx.test.rules) - api(libs.androidx.compose.ui.test) + api(libs.androidx.test.runner) api(libs.hilt.android.testing) + api(libs.junit4) + api(libs.kotlinx.coroutines.test) + api(libs.turbine) debugApi(libs.androidx.compose.ui.testManifest) + + implementation(project(":core:common")) + implementation(project(":core:data")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:notifications")) + implementation(libs.kotlinx.datetime) } diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/notifications/TestNotifier.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/notifications/TestNotifier.kt new file mode 100644 index 000000000..669d2e6c4 --- /dev/null +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/notifications/TestNotifier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.core.testing.notifications + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.notifications.Notifier + +/** + * Aggregates news resources that have been notified for addition + */ +class TestNotifier : Notifier { + + private val mutableAddedNewResources = mutableListOf>() + + val addedNewsResources: List> = mutableAddedNewResources + + override fun onNewsAdded(newsResources: List) { + mutableAddedNewResources.add(newsResources) + } +} diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncStatusMonitor.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt similarity index 85% rename from core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncStatusMonitor.kt rename to core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt index a2edc89ff..999b67195 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncStatusMonitor.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt @@ -16,16 +16,20 @@ package com.google.samples.apps.nowinandroid.core.testing.util -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class TestSyncStatusMonitor : SyncStatusMonitor { +class TestSyncManager : SyncManager { private val syncStatusFlow = MutableStateFlow(false) override val isSyncing: Flow = syncStatusFlow + override fun requestSync() { + TODO("Not yet implemented") + } + /** * A test-only API to set the sync status from tests. */ diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 0438b8f36..b7280e757 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -27,28 +27,28 @@ android { } dependencies { - implementation(project(":core:designsystem")) - implementation(project(":core:model")) - implementation(project(":core:domain")) - implementation(project(":core:analytics")) - - implementation(libs.androidx.browser) - implementation(libs.androidx.core.ktx) - implementation(libs.coil.kt) - implementation(libs.coil.kt.compose) - implementation(libs.kotlinx.datetime) - api(libs.androidx.compose.foundation) api(libs.androidx.compose.foundation.layout) api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material3) - debugApi(libs.androidx.compose.ui.tooling) - api(libs.androidx.compose.ui.tooling.preview) - api(libs.androidx.compose.ui.util) api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime.livedata) + api(libs.androidx.compose.ui.tooling.preview) + api(libs.androidx.compose.ui.util) api(libs.androidx.metrics) api(libs.androidx.tracing.ktx) + debugApi(libs.androidx.compose.ui.tooling) + + implementation(project(":core:analytics")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(libs.androidx.browser) + implementation(libs.androidx.core.ktx) + implementation(libs.coil.kt) + implementation(libs.coil.kt.compose) + implementation(libs.kotlinx.datetime) + androidTestImplementation(project(":core:testing")) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index 3b0015bab..df9b5dab8 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -21,6 +21,7 @@ import android.net.Uri import androidx.annotation.ColorInt import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope @@ -31,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices @@ -78,6 +80,7 @@ fun LazyGridScope.newsFeed( ) }, onTopicClick = onTopicClick, + modifier = Modifier.padding(horizontal = 8.dp), ) } } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 3e0bb5784..0201887a3 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -159,7 +159,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { contentDescription = null, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(48.dp)) Text( text = stringResource(id = R.string.bookmarks_empty_error), diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/build.gradle.kts index ed7be27dc..4fa032a88 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/build.gradle.kts @@ -27,7 +27,6 @@ android { } dependencies { - implementation(libs.kotlinx.datetime) - implementation(libs.accompanist.flowlayout) + implementation(libs.kotlinx.datetime) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index aa4dc5f26..fe549b79e 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells @@ -247,7 +248,7 @@ private fun LazyGridScope.onboarding( text = stringResource(R.string.onboarding_guidance_subtitle), modifier = Modifier .fillMaxWidth() - .padding(top = 8.dp, start = 16.dp, end = 16.dp), + .padding(top = 8.dp, start = 24.dp, end = 24.dp), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, ) @@ -265,8 +266,9 @@ private fun LazyGridScope.onboarding( onClick = saveFollowedTopics, enabled = onboardingUiState.isDismissable, modifier = Modifier - .padding(horizontal = 40.dp) - .width(364.dp), + .padding(horizontal = 24.dp) + .widthIn(364.dp) + .fillMaxWidth(), ) { Text( text = stringResource(R.string.done), @@ -453,7 +455,8 @@ fun ForYouScreenTopicSelection( ForYouScreen( isSyncing = false, onboardingUiState = OnboardingUiState.Shown( - topics = userNewsResources.flatMap { news -> news.followableTopics }, + topics = userNewsResources.flatMap { news -> news.followableTopics } + .distinctBy { it.topic.id }, ), feedState = NewsFeedUiState.Success( feed = userNewsResources, diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 085593932..376624376 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource @@ -41,7 +41,7 @@ import javax.inject.Inject @HiltViewModel class ForYouViewModel @Inject constructor( - syncStatusMonitor: SyncStatusMonitor, + syncManager: SyncManager, private val userDataRepository: UserDataRepository, getUserNewsResources: GetUserNewsResourcesUseCase, getFollowableTopics: GetFollowableTopicsUseCase, @@ -50,7 +50,7 @@ class ForYouViewModel @Inject constructor( private val shouldShowOnboarding: Flow = userDataRepository.userData.map { !it.shouldHideOnboarding } - val isSyncing = syncStatusMonitor.isSyncing + val isSyncing = syncManager.isSyncing .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 9e51758f0..db8178b89 100644 --- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -30,7 +30,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor -import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @@ -52,7 +52,7 @@ class ForYouViewModelTest { val mainDispatcherRule = MainDispatcherRule() private val networkMonitor = TestNetworkMonitor() - private val syncStatusMonitor = TestSyncStatusMonitor() + private val syncManager = TestSyncManager() private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() @@ -70,7 +70,7 @@ class ForYouViewModelTest { @Before fun setup() { viewModel = ForYouViewModel( - syncStatusMonitor = syncStatusMonitor, + syncManager = syncManager, userDataRepository = userDataRepository, getUserNewsResources = getUserNewsResourcesUseCase, getFollowableTopics = getFollowableTopicsUseCase, @@ -106,7 +106,7 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenAppIsSyncingWithNoInterests() = runTest { - syncStatusMonitor.setSyncing(true) + syncManager.setSyncing(true) val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.isSyncing.collect() } diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index f8a7a8d90..ec9fd8f10 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -65,7 +65,7 @@ fun InterestsItem( .padding(vertical = itemSeparation), ) { InterestsIcon(topicImageUrl, iconModifier.size(64.dp)) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(24.dp)) InterestContent(name, description) } NiaIconToggleButton( diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index dcca35795..71667e4dc 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -38,9 +38,9 @@ fun TopicsTabContent( ) { LazyColumn( modifier = modifier - .padding(horizontal = 16.dp) + .padding(horizontal = 24.dp) .testTag("interests:topics"), - contentPadding = PaddingValues(top = 8.dp), + contentPadding = PaddingValues(vertical = 16.dp), ) { topics.forEach { followableTopic -> val topicId = followableTopic.topic.id diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6444cd40e..ec323ed85 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,19 +93,20 @@ androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.r androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" } -androidx-tracing-ktx = { group = "androidx.tracing", name="tracing-ktx", version.ref = "androidxTracing" } +androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } androidx-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" } androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" } -firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref="firebaseBom"} -firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx"} -firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx"} -firebase-crashlytics-gradle = { group = "com.google.firebase", name="firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin"} -firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx"} -firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin"} +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } +firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } +firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } +firebase-crashlytics-gradle = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" } +firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx" } +firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } @@ -137,9 +138,9 @@ ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devto android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin"} -firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin"} -gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin"} +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } +firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } +gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 857f9d56c..2af582a7b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,6 +47,7 @@ include(":core:network") include(":core:ui") include(":core:testing") include(":core:analytics") +include(":core:notifications") include(":feature:foryou") include(":feature:interests") diff --git a/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncStatusMonitor.kt b/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncManager.kt similarity index 82% rename from sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncStatusMonitor.kt rename to sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncManager.kt index 647dd864e..2b0b4fb6a 100644 --- a/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncStatusMonitor.kt +++ b/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/NeverSyncingSyncManager.kt @@ -16,11 +16,12 @@ package com.google.samples.apps.nowinandroid.core.sync.test -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject -class NeverSyncingSyncStatusMonitor @Inject constructor() : SyncStatusMonitor { +class NeverSyncingSyncManager @Inject constructor() : SyncManager { override val isSyncing: Flow = flowOf(false) + override fun requestSync() = Unit } diff --git a/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt b/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt index 323704b5a..0089450b5 100644 --- a/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt +++ b/sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt @@ -16,7 +16,7 @@ package com.google.samples.apps.nowinandroid.core.sync.test -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import com.google.samples.apps.nowinandroid.sync.di.SyncModule import dagger.Binds import dagger.Module @@ -31,6 +31,6 @@ import dagger.hilt.testing.TestInstallIn interface TestSyncModule { @Binds fun bindsSyncStatusMonitor( - syncStatusMonitor: NeverSyncingSyncStatusMonitor, - ): SyncStatusMonitor + syncStatusMonitor: NeverSyncingSyncManager, + ): SyncManager } diff --git a/sync/work/build.gradle.kts b/sync/work/build.gradle.kts index a3b589db3..fa7835323 100644 --- a/sync/work/build.gradle.kts +++ b/sync/work/build.gradle.kts @@ -27,24 +27,23 @@ android { } dependencies { + implementation(project(":core:analytics")) implementation(project(":core:common")) - implementation(project(":core:model")) implementation(project(":core:data")) implementation(project(":core:datastore")) - implementation(project(":core:analytics")) - - implementation(libs.kotlinx.coroutines.android) - + implementation(project(":core:model")) implementation(libs.androidx.lifecycle.livedata.ktx) - implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.startup) + implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.work.ktx) + implementation(libs.firebase.cloud.messaging) implementation(libs.hilt.ext.work) - - testImplementation(project(":core:testing")) - androidTestImplementation(project(":core:testing")) + implementation(libs.kotlinx.coroutines.android) kapt(libs.hilt.ext.compiler) + testImplementation(project(":core:testing")) + + androidTestImplementation(project(":core:testing")) androidTestImplementation(libs.androidx.work.testing) } diff --git a/sync/work/src/demo/AndroidManifest.xml b/sync/work/src/demo/AndroidManifest.xml new file mode 100644 index 000000000..dac61a5bc --- /dev/null +++ b/sync/work/src/demo/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/sync/work/src/main/AndroidManifest.xml b/sync/work/src/main/AndroidManifest.xml index 2487eb105..0d0b720bb 100644 --- a/sync/work/src/main/AndroidManifest.xml +++ b/sync/work/src/main/AndroidManifest.xml @@ -29,7 +29,13 @@ android:value="androidx.startup" tools:node="remove" /> - + + + + + diff --git a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/di/SyncModule.kt b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/di/SyncModule.kt index 68f9eee93..bbc45dc42 100644 --- a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/di/SyncModule.kt +++ b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/di/SyncModule.kt @@ -16,8 +16,8 @@ package com.google.samples.apps.nowinandroid.sync.di -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor -import com.google.samples.apps.nowinandroid.sync.status.WorkManagerSyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager +import com.google.samples.apps.nowinandroid.sync.status.WorkManagerSyncManager import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -28,6 +28,6 @@ import dagger.hilt.components.SingletonComponent interface SyncModule { @Binds fun bindsSyncStatusMonitor( - syncStatusMonitor: WorkManagerSyncStatusMonitor, - ): SyncStatusMonitor + syncStatusMonitor: WorkManagerSyncManager, + ): SyncManager } diff --git a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/services/SyncNotificationsService.kt b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/services/SyncNotificationsService.kt new file mode 100644 index 000000000..ab318776a --- /dev/null +++ b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/services/SyncNotificationsService.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.google.samples.apps.nowinandroid.sync.services + +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +private const val SYNC_TOPIC = "sync" + +@AndroidEntryPoint +class SyncNotificationsService : FirebaseMessagingService() { + + @Inject + lateinit var syncManager: SyncManager + + override fun onMessageReceived(message: RemoteMessage) { + if (SYNC_TOPIC == message.from) { + syncManager.requestSync() + } + } +} diff --git a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncStatusMonitor.kt b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncManager.kt similarity index 67% rename from sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncStatusMonitor.kt rename to sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncManager.kt index f4f9d02cb..9bb57ccf0 100644 --- a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncStatusMonitor.kt +++ b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/WorkManagerSyncManager.kt @@ -19,27 +19,39 @@ package com.google.samples.apps.nowinandroid.sync.status import android.content.Context import androidx.lifecycle.asFlow import androidx.lifecycle.map +import androidx.work.ExistingWorkPolicy import androidx.work.WorkInfo import androidx.work.WorkInfo.State import androidx.work.WorkManager -import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor +import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import com.google.samples.apps.nowinandroid.sync.initializers.SyncWorkName +import com.google.samples.apps.nowinandroid.sync.workers.SyncWorker import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import javax.inject.Inject /** - * [SyncStatusMonitor] backed by [WorkInfo] from [WorkManager] + * [SyncManager] backed by [WorkInfo] from [WorkManager] */ -class WorkManagerSyncStatusMonitor @Inject constructor( - @ApplicationContext context: Context, -) : SyncStatusMonitor { +class WorkManagerSyncManager @Inject constructor( + @ApplicationContext private val context: Context, +) : SyncManager { override val isSyncing: Flow = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(SyncWorkName) .map(MutableList::anyRunning) .asFlow() .conflate() + + override fun requestSync() { + val workManager = WorkManager.getInstance(context) + // Run sync on app startup and ensure only one sync worker runs at any time + workManager.enqueueUniqueWork( + SyncWorkName, + ExistingWorkPolicy.KEEP, + SyncWorker.startUpSyncWork(), + ) + } } private val List.anyRunning get() = any { it.state == State.RUNNING }