From 08956492c84ea9b4c57ffdd69c9a4eef9de47b4a Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Tue, 21 Mar 2023 09:37:24 -0400 Subject: [PATCH] Backend triggered sync Change-Id: I53c43b136ebb755f6258b1e815301dddb3b536a3 --- core/data/build.gradle.kts | 1 + .../repository/OfflineFirstNewsRepository.kt | 27 +++++++++++++ .../{SyncStatusMonitor.kt => SyncManager.kt} | 3 +- .../OfflineFirstNewsRepositoryTest.kt | 24 ++++++++++++ core/notifications/.gitignore | 1 + core/notifications/build.gradle.kts | 35 +++++++++++++++++ .../core/notifications/NotificationsModule.kt | 31 +++++++++++++++ .../src/main/AndroidManifest.xml | 17 +++++++++ .../notifications/AndroidSystemNotifier.kt | 32 ++++++++++++++++ .../core/notifications/NoOpNotifier.kt | 27 +++++++++++++ .../core/notifications/Notifier.kt | 26 +++++++++++++ .../src/main/res/values/strings.xml | 22 +++++++++++ .../core/notifications/NotificationsModule.kt | 31 +++++++++++++++ core/testing/build.gradle.kts | 1 + .../testing/notifications/TestNotifier.kt | 34 +++++++++++++++++ ...yncStatusMonitor.kt => TestSyncManager.kt} | 8 +++- .../feature/foryou/ForYouViewModel.kt | 6 +-- .../feature/foryou/ForYouViewModelTest.kt | 8 ++-- gradle/libs.versions.toml | 21 +++++----- settings.gradle.kts | 1 + ...sMonitor.kt => NeverSyncingSyncManager.kt} | 5 ++- .../core/sync/test/TestSyncModule.kt | 6 +-- sync/work/build.gradle.kts | 1 + sync/work/src/demo/AndroidManifest.xml | 38 +++++++++++++++++++ sync/work/src/main/AndroidManifest.xml | 8 +++- .../apps/nowinandroid/sync/di/SyncModule.kt | 8 ++-- .../sync/services/SyncNotificationsService.kt | 38 +++++++++++++++++++ ...usMonitor.kt => WorkManagerSyncManager.kt} | 22 ++++++++--- 28 files changed, 447 insertions(+), 35 deletions(-) rename core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/{SyncStatusMonitor.kt => SyncManager.kt} (94%) create mode 100644 core/notifications/.gitignore create mode 100644 core/notifications/build.gradle.kts create mode 100644 core/notifications/src/demo/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt create mode 100644 core/notifications/src/main/AndroidManifest.xml create mode 100644 core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt create mode 100644 core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/NoOpNotifier.kt create mode 100644 core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/Notifier.kt create mode 100644 core/notifications/src/main/res/values/strings.xml create mode 100644 core/notifications/src/prod/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt create mode 100644 core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/notifications/TestNotifier.kt rename core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/util/{TestSyncStatusMonitor.kt => TestSyncManager.kt} (85%) rename sync/sync-test/src/main/java/com/google/samples/apps/nowinandroid/core/sync/test/{NeverSyncingSyncStatusMonitor.kt => NeverSyncingSyncManager.kt} (82%) create mode 100644 sync/work/src/demo/AndroidManifest.xml create mode 100644 sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/services/SyncNotificationsService.kt rename sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/status/{WorkManagerSyncStatusMonitor.kt => WorkManagerSyncManager.kt} (67%) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e4b265649..5d34aac2c 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(project(":core:datastore")) implementation(project(":core:model")) implementation(project(":core:network")) + implementation(project(":core:notifications")) implementation(libs.androidx.core.ktx) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.datetime) 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/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 4e87bb039..2fea5beb3 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -40,5 +40,6 @@ dependencies { 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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 737ad43a0..8826089a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,19 +94,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" } @@ -138,9 +139,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 b5b3bdb68..fa7835323 100644 --- a/sync/work/build.gradle.kts +++ b/sync/work/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(libs.androidx.startup) implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.work.ktx) + implementation(libs.firebase.cloud.messaging) implementation(libs.hilt.ext.work) implementation(libs.kotlinx.coroutines.android) 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 }