Skip to content

Commit 08a165c

Browse files
authored
Merge pull request #631 from android/tj/notifications
Backend triggered sync
2 parents ba6a697 + 0895649 commit 08a165c

File tree

28 files changed

+447
-35
lines changed

28 files changed

+447
-35
lines changed

core/data/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies {
3636
implementation(project(":core:datastore"))
3737
implementation(project(":core:model"))
3838
implementation(project(":core:network"))
39+
implementation(project(":core:notifications"))
3940
implementation(libs.androidx.core.ktx)
4041
implementation(libs.kotlinx.coroutines.android)
4142
implementation(libs.kotlinx.datetime)

core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
3030
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
3131
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
3232
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
33+
import com.google.samples.apps.nowinandroid.core.notifications.Notifier
3334
import kotlinx.coroutines.flow.Flow
35+
import kotlinx.coroutines.flow.first
3436
import kotlinx.coroutines.flow.map
3537
import javax.inject.Inject
3638

@@ -46,6 +48,7 @@ class OfflineFirstNewsRepository @Inject constructor(
4648
private val newsResourceDao: NewsResourceDao,
4749
private val topicDao: TopicDao,
4850
private val network: NiaNetworkDataSource,
51+
private val notifier: Notifier,
4952
) : NewsRepository {
5053

5154
override fun getNewsResources(
@@ -69,6 +72,16 @@ class OfflineFirstNewsRepository @Inject constructor(
6972
},
7073
modelDeleter = newsResourceDao::deleteNewsResources,
7174
modelUpdater = { changedIds ->
75+
// TODO: Make this more efficient, there is no need to retrieve populated
76+
// news resources when all that's needed are the ids
77+
val existingNewsResourceIds = newsResourceDao.getNewsResources(
78+
useFilterNewsIds = true,
79+
filterNewsIds = changedIds.toSet(),
80+
)
81+
.first()
82+
.map { it.entity.id }
83+
.toSet()
84+
7285
changedIds.chunked(SYNC_BATCH_SIZE).forEach { chunkedIds ->
7386
val networkNewsResources = network.getNewsResources(ids = chunkedIds)
7487

@@ -92,6 +105,20 @@ class OfflineFirstNewsRepository @Inject constructor(
92105
.flatten(),
93106
)
94107
}
108+
109+
val addedNewsResources = newsResourceDao.getNewsResources(
110+
useFilterNewsIds = true,
111+
filterNewsIds = changedIds.toSet(),
112+
)
113+
.first()
114+
.filter { !existingNewsResourceIds.contains(it.entity.id) }
115+
.map(PopulatedNewsResource::asExternalModel)
116+
117+
// TODO: Define business logic for notifications on first time sync.
118+
// we probably do not want to send notifications on first install.
119+
// We can easily check if the change list version is 0 and not send notifications
120+
// if it is.
121+
if (addedNewsResources.isNotEmpty()) notifier.onNewsAdded(addedNewsResources)
95122
},
96123
)
97124
}

core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncStatusMonitor.kt renamed to core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/SyncManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow
2121
/**
2222
* Reports on if synchronization is in progress
2323
*/
24-
interface SyncStatusMonitor {
24+
interface SyncManager {
2525
val isSyncing: Flow<Boolean>
26+
fun requestSync()
2627
}

core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen
3636
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
3737
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
3838
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
39+
import com.google.samples.apps.nowinandroid.core.testing.notifications.TestNotifier
3940
import kotlinx.coroutines.flow.first
4041
import kotlinx.coroutines.test.TestScope
4142
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -58,6 +59,8 @@ class OfflineFirstNewsRepositoryTest {
5859

5960
private lateinit var network: TestNiaNetworkDataSource
6061

62+
private lateinit var notifier: TestNotifier
63+
6164
private lateinit var synchronizer: Synchronizer
6265

6366
@get:Rule
@@ -68,6 +71,7 @@ class OfflineFirstNewsRepositoryTest {
6871
newsResourceDao = TestNewsResourceDao()
6972
topicDao = TestTopicDao()
7073
network = TestNiaNetworkDataSource()
74+
notifier = TestNotifier()
7175
synchronizer = TestSynchronizer(
7276
NiaPreferencesDataSource(
7377
tmpFolder.testUserPreferencesDataStore(testScope),
@@ -78,6 +82,7 @@ class OfflineFirstNewsRepositoryTest {
7882
newsResourceDao = newsResourceDao,
7983
topicDao = topicDao,
8084
network = network,
85+
notifier = notifier,
8186
)
8287
}
8388

@@ -145,6 +150,12 @@ class OfflineFirstNewsRepositoryTest {
145150
expected = network.latestChangeListVersion(CollectionType.NewsResources),
146151
actual = synchronizer.getChangeListVersions().newsResourceVersion,
147152
)
153+
154+
// Notifier should have been called with new news resources
155+
assertEquals(
156+
expected = newsResourcesFromDb.map(NewsResource::id).sorted(),
157+
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
158+
)
148159
}
149160

150161
@Test
@@ -186,6 +197,13 @@ class OfflineFirstNewsRepositoryTest {
186197
expected = network.latestChangeListVersion(CollectionType.NewsResources),
187198
actual = synchronizer.getChangeListVersions().newsResourceVersion,
188199
)
200+
201+
// Notifier should have been called with news resources from network that are not
202+
// deleted
203+
assertEquals(
204+
expected = (newsResourcesFromNetwork.map(NewsResource::id) - deletedItems).sorted(),
205+
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
206+
)
189207
}
190208

191209
@Test
@@ -225,6 +243,12 @@ class OfflineFirstNewsRepositoryTest {
225243
expected = changeList.last().changeListVersion,
226244
actual = synchronizer.getChangeListVersions().newsResourceVersion,
227245
)
246+
247+
// Notifier should have been called with only added news resources from network
248+
assertEquals(
249+
expected = newsResourcesFromNetwork.map(NewsResource::id).sorted(),
250+
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
251+
)
228252
}
229253

230254
@Test

core/notifications/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
plugins {
17+
id("nowinandroid.android.library")
18+
id("nowinandroid.android.library.compose")
19+
id("nowinandroid.android.hilt")
20+
}
21+
22+
android {
23+
namespace = "com.google.samples.apps.nowinandroid.core.notifications"
24+
}
25+
26+
dependencies {
27+
implementation(project(":core:model"))
28+
29+
implementation(libs.kotlinx.coroutines.android)
30+
implementation(libs.androidx.compose.runtime)
31+
implementation(libs.androidx.core.ktx)
32+
33+
implementation(platform(libs.firebase.bom))
34+
implementation(libs.firebase.cloud.messaging)
35+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.core.notifications
18+
19+
import dagger.Binds
20+
import dagger.Module
21+
import dagger.hilt.InstallIn
22+
import dagger.hilt.components.SingletonComponent
23+
24+
@Module
25+
@InstallIn(SingletonComponent::class)
26+
abstract class NotificationsModule {
27+
@Binds
28+
abstract fun bindNotifier(
29+
notifier: NoOpNotifier,
30+
): Notifier
31+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2023 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.core.notifications
18+
19+
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
20+
import javax.inject.Inject
21+
import javax.inject.Singleton
22+
23+
/**
24+
* Implementation of [Notifier] that displays notifications in the system tray.
25+
*/
26+
@Singleton
27+
class AndroidSystemNotifier @Inject constructor() : Notifier {
28+
29+
override fun onNewsAdded(newsResources: List<NewsResource>) {
30+
// TODO, create notification and display to the user
31+
}
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.core.notifications
18+
19+
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
20+
import javax.inject.Inject
21+
22+
/**
23+
* Implementation of [Notifier] which does nothing. Useful for tests and previews.
24+
*/
25+
class NoOpNotifier @Inject constructor() : Notifier {
26+
override fun onNewsAdded(newsResources: List<NewsResource>) = Unit
27+
}

0 commit comments

Comments
 (0)