Handle items deleted on server during sync

Change-Id: Ifa2250d9ce4b3dedf10804554a39fb4d62ffed9d
pull/2/head
Adetunji Dahunsi 2 years ago committed by Don Turner
parent f97c8fdef5
commit 36c74ddd35

@ -305,6 +305,49 @@ class NewsResourceDaoTest {
filteredNewsResources.map { it.entity.id }
)
}
@Test
fun newsResourceDao_deletes_items_by_ids() =
runTest {
val newsResourceEntities = listOf(
testNewsResource(
id = 0,
millisSinceEpoch = 0,
),
testNewsResource(
id = 1,
millisSinceEpoch = 3,
),
testNewsResource(
id = 2,
millisSinceEpoch = 1,
),
testNewsResource(
id = 3,
millisSinceEpoch = 2,
),
)
val episodeEntityShells = newsResourceEntities
.map(NewsResourceEntity::episodeEntityShell)
.distinct()
episodeDao.upsertEpisodes(episodeEntityShells)
newsResourceDao.upsertNewsResources(newsResourceEntities)
val (toDelete, toKeep) = newsResourceEntities.partition { it.id % 2 == 0 }
newsResourceDao.deleteNewsResources(
toDelete.map(NewsResourceEntity::id)
)
assertEquals(
toKeep.map(NewsResourceEntity::id)
.toSet(),
newsResourceDao.getNewsResourcesStream().first()
.map { it.entity.id }
.toSet()
)
}
}
private fun testAuthorEntity(

@ -54,4 +54,15 @@ interface AuthorDao {
insertMany = ::insertOrIgnoreAuthors,
updateMany = ::updateAuthors
)
/**
* Deletes rows in the db matching the specified [ids]
*/
@Query(
value = """
DELETE FROM authors
WHERE id in (:ids)
"""
)
suspend fun deleteAuthors(ids: List<Int>)
}

@ -56,4 +56,15 @@ interface EpisodeDao {
insertMany = ::insertOrIgnoreEpisodes,
updateMany = ::updateEpisodes
)
/**
* Deletes rows in the db matching the specified [ids]
*/
@Query(
value = """
DELETE FROM episodes
WHERE id in (:ids)
"""
)
suspend fun deleteEpisodes(ids: List<Int>)
}

@ -94,4 +94,15 @@ interface NewsResourceDao {
suspend fun insertOrIgnoreAuthorCrossRefEntities(
newsResourceAuthorCrossReferences: List<NewsResourceAuthorCrossRef>
)
/**
* Deletes rows in the db matching the specified [ids]
*/
@Query(
value = """
DELETE FROM news_resources
WHERE id in (:ids)
"""
)
suspend fun deleteNewsResources(ids: List<Int>)
}

@ -70,4 +70,15 @@ interface TopicDao {
insertMany = ::insertOrIgnoreTopics,
updateMany = ::updateTopics
)
/**
* Deletes rows in the db matching the specified [ids]
*/
@Query(
value = """
DELETE FROM topics
WHERE id in (:ids)
"""
)
suspend fun deleteTopics(ids: List<Int>)
}

@ -18,12 +18,38 @@ package com.google.samples.apps.nowinandroid.core.domain
import android.util.Log
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
/**
* Interface marker for a class that manages synchronization between local data and a remote
* source for a [Syncable].
*/
interface Synchronizer {
suspend fun getChangeListVersions(): ChangeListVersions
suspend fun updateChangeListVersions(update: ChangeListVersions.() -> ChangeListVersions)
/**
* Syntactic sugar to call [Syncable.syncWith] while omitting the synchronizer argument
*/
suspend fun Syncable.sync() = this@sync.syncWith(this@Synchronizer)
}
/**
* Interface marker for a class that is synchronized with a remote source. Syncing must not be
* performed concurrently and it is the [Synchronizer]'s responsibility to ensure this.
*/
interface Syncable {
/**
* Synchronizes the local database in backing the repository with the network.
* Returns if the sync was successful or not.
*/
suspend fun syncWith(synchronizer: Synchronizer): Boolean
}
/**
* Attempts [block], returning a successful [Result] if it succeeds, otherwise a [Result.Failure]
* taking care not to break structured concurrency
@ -46,30 +72,35 @@ private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> =
* [versionReader] Reads the current version of the model that needs to be synced
* [changeListFetcher] Fetches the change list for the model
* [versionUpdater] Updates the [ChangeListVersions] after a successful sync
* [modelUpdater] Updates the model by consuming the ids of the models that have changed.
* [modelDeleter] Deletes models by consuming the ids of the models that have been deleted.
* [modelUpdater] Updates models by consuming the ids of the models that have changed.
*
* Note that the blocks defined above may be run concurrently due to concurrent calls
* to [changeListSync]. It is the caller's responsibility to make [modelUpdater] performs atomic
* operations.
* Note that the blocks defined above are never run concurrently, and the [Synchronizer]
* implementation must guarantee this.
*/
suspend fun changeListSync(
niaPreferences: NiaPreferences,
suspend fun Synchronizer.changeListSync(
versionReader: (ChangeListVersions) -> Int,
changeListFetcher: suspend (Int) -> List<NetworkChangeList>,
versionUpdater: ChangeListVersions.(Int) -> ChangeListVersions,
modelUpdater: suspend (List<Int>) -> Unit
modelDeleter: suspend (List<Int>) -> Unit,
modelUpdater: suspend (List<Int>) -> Unit,
) = suspendRunCatching {
// Fetch the change list since last sync (akin to a git fetch)
val currentVersion = versionReader(niaPreferences.getChangeListVersions())
val currentVersion = versionReader(getChangeListVersions())
val changeList = changeListFetcher(currentVersion)
if (changeList.isEmpty()) return@suspendRunCatching true
val (deleted, updated) = changeList.partition(NetworkChangeList::isDelete)
// Delete models that have been deleted server-side
modelDeleter(deleted.map(NetworkChangeList::id))
// Using the change list, pull down and save the changes (akin to a git pull)
modelUpdater(changeList.map(NetworkChangeList::id))
modelUpdater(updated.map(NetworkChangeList::id))
// Update the last synced version (akin to updating local git HEAD)
val latestVersion = changeList.last().changeListVersion
niaPreferences.updateChangeListVersion {
updateChangeListVersions {
versionUpdater(latestVersion)
}
}.isSuccess

@ -16,10 +16,11 @@
package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.domain.Syncable
import com.google.samples.apps.nowinandroid.core.model.data.Author
import kotlinx.coroutines.flow.Flow
interface AuthorsRepository {
interface AuthorsRepository : Syncable {
/**
* Gets the available Authors as a stream
*/
@ -39,10 +40,4 @@ interface AuthorsRepository {
* Returns the users currently followed authors
*/
fun getFollowedAuthorIdsStream(): Flow<Set<Int>>
/**
* Synchronizes the local database in backing the repository with the network.
* Returns if the sync was successful or not.
*/
suspend fun sync(): Boolean
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.model.data.Author
@ -51,20 +52,21 @@ class LocalAuthorsRepository @Inject constructor(
override fun getFollowedAuthorIdsStream(): Flow<Set<Int>> = niaPreferences.followedAuthorIds
override suspend fun sync(): Boolean = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::authorVersion,
changeListFetcher = { currentVersion ->
network.getAuthorChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(authorVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkAuthors = network.getAuthors(ids = changedIds)
authorDao.upsertAuthors(
entities = networkAuthors.map(NetworkAuthor::asEntity)
)
}
)
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync(
versionReader = ChangeListVersions::authorVersion,
changeListFetcher = { currentVersion ->
network.getAuthorChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(authorVersion = latestVersion)
},
modelDeleter = authorDao::deleteAuthors,
modelUpdater = { changedIds ->
val networkAuthors = network.getAuthors(ids = changedIds)
authorDao.upsertAuthors(
entities = networkAuthors.map(NetworkAuthor::asEntity)
)
}
)
}

@ -26,7 +26,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsRes
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.model.authorCrossReferences
@ -50,7 +50,6 @@ class LocalNewsRepository @Inject constructor(
private val authorDao: AuthorDao,
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : NewsRepository {
override fun getNewsResourcesStream(): Flow<List<NewsResource>> =
@ -66,53 +65,54 @@ class LocalNewsRepository @Inject constructor(
)
.map { it.map(PopulatedNewsResource::asExternalModel) }
override suspend fun sync() = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
override suspend fun syncWith(synchronizer: Synchronizer) =
synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
// Order of invocation matters to satisfy id and foreign key constraints!
// Order of invocation matters to satisfy id and foreign key constraints!
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
authorDao.insertOrIgnoreAuthors(
authorEntities = networkNewsResources
.map(NetworkNewsResource::authorEntityShells)
.flatten()
.distinctBy(AuthorEntity::id)
)
episodeDao.insertOrIgnoreEpisodes(
episodeEntities = networkNewsResources
.map(NetworkNewsResource::episodeEntityShell)
.distinctBy(EpisodeEntity::id)
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.flatten()
)
newsResourceDao.insertOrIgnoreAuthorCrossRefEntities(
newsResourceAuthorCrossReferences = networkNewsResources
.map(NetworkNewsResource::authorCrossReferences)
.distinct()
.flatten()
)
}
)
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
authorDao.insertOrIgnoreAuthors(
authorEntities = networkNewsResources
.map(NetworkNewsResource::authorEntityShells)
.flatten()
.distinctBy(AuthorEntity::id)
)
episodeDao.insertOrIgnoreEpisodes(
episodeEntities = networkNewsResources
.map(NetworkNewsResource::episodeEntityShell)
.distinctBy(EpisodeEntity::id)
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.flatten()
)
newsResourceDao.insertOrIgnoreAuthorCrossRefEntities(
newsResourceAuthorCrossReferences = networkNewsResources
.map(NetworkNewsResource::authorCrossReferences)
.distinct()
.flatten()
)
}
)
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.model.data.Topic
@ -54,20 +55,21 @@ class LocalTopicsRepository @Inject constructor(
override fun getFollowedTopicIdsStream() = niaPreferences.followedTopicIds
override suspend fun sync(): Boolean = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::topicVersion,
changeListFetcher = { currentVersion ->
network.getTopicChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(topicVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity)
)
}
)
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync(
versionReader = ChangeListVersions::topicVersion,
changeListFetcher = { currentVersion ->
network.getTopicChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(topicVersion = latestVersion)
},
modelDeleter = topicDao::deleteTopics,
modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity)
)
}
)
}

@ -16,13 +16,14 @@
package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.domain.Syncable
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import kotlinx.coroutines.flow.Flow
/**
* Data layer implementation for [NewsResource]
*/
interface NewsRepository {
interface NewsRepository : Syncable {
/**
* Returns available news resources as a stream.
*/
@ -35,10 +36,4 @@ interface NewsRepository {
filterAuthorIds: Set<Int> = emptySet(),
filterTopicIds: Set<Int> = emptySet(),
): Flow<List<NewsResource>>
/**
* Synchronizes the local database in backing the repository with the network.
* Returns if the sync was successful or not.
*/
suspend fun sync(): Boolean
}

@ -16,10 +16,11 @@
package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.domain.Syncable
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.coroutines.flow.Flow
interface TopicsRepository {
interface TopicsRepository : Syncable {
/**
* Gets the available topics as a stream
*/
@ -44,10 +45,4 @@ interface TopicsRepository {
* Returns the users currently followed topics
*/
fun getFollowedTopicIdsStream(): Flow<Set<Int>>
/**
* Synchronizes the local database in backing the repository with the network.
* Returns if the sync was successful or not.
*/
suspend fun sync(): Boolean
}

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.domain.repository.fake
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
@ -68,5 +69,5 @@ class FakeAuthorsRepository @Inject constructor(
override fun getFollowedAuthorIdsStream(): Flow<Set<Int>> = niaPreferences.followedAuthorIds
override suspend fun sync() = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.domain.repository.fake
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -71,5 +72,5 @@ class FakeNewsRepository @Inject constructor(
}
.flowOn(ioDispatcher)
override suspend fun sync() = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.domain.repository.fake
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
@ -72,5 +73,5 @@ class FakeTopicsRepository @Inject constructor(
override fun getFollowedTopicIdsStream() = niaPreferences.followedTopicIds
override suspend fun sync() = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -21,10 +21,14 @@ import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.CollectionType
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestAuthorDao
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestNiaNetwork
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert
@ -41,7 +45,7 @@ class LocalAuthorsRepositoryTest {
private lateinit var network: TestNiaNetwork
private lateinit var niaPreferences: NiaPreferences
private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@ -50,9 +54,10 @@ class LocalAuthorsRepositoryTest {
fun setup() {
authorDao = TestAuthorDao()
network = TestNiaNetwork()
niaPreferences = NiaPreferences(
val niaPreferences = NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
)
synchronizer = TestSynchronizer(niaPreferences)
subject = LocalAuthorsRepository(
authorDao = authorDao,
@ -76,23 +81,23 @@ class LocalAuthorsRepositoryTest {
@Test
fun localAuthorsRepository_sync_pulls_from_network() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
val network = network.getAuthors()
val networkAuthors = network.getAuthors()
.map(NetworkAuthor::asEntity)
val db = authorDao.getAuthorEntitiesStream()
val dbAuthors = authorDao.getAuthorEntitiesStream()
.first()
Assert.assertEquals(
network.map(AuthorEntity::id),
db.map(AuthorEntity::id)
networkAuthors.map(AuthorEntity::id),
dbAuthors.map(AuthorEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex,
niaPreferences.getChangeListVersions().authorVersion
network.latestChangeListVersion(CollectionType.Authors),
synchronizer.getChangeListVersions().authorVersion
)
}
@ -100,16 +105,23 @@ class LocalAuthorsRepositoryTest {
fun localAuthorsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set author version to 5
niaPreferences.updateChangeListVersion {
synchronizer.updateChangeListVersions {
copy(authorVersion = 5)
}
subject.sync()
subject.syncWith(synchronizer)
val changeList = network.changeListsAfter(
CollectionType.Authors,
version = 5
)
val changeListIds = changeList
.map(NetworkChangeList::id)
.toSet()
val network = network.getAuthors()
.map(NetworkAuthor::asEntity)
// Drop 5 to simulate the first 5 items being unchanged
.drop(5)
.filter { it.id in changeListIds }
val db = authorDao.getAuthorEntitiesStream()
.first()
@ -121,8 +133,47 @@ class LocalAuthorsRepositoryTest {
// After sync version should be updated
Assert.assertEquals(
network.lastIndex + 5,
niaPreferences.getChangeListVersions().authorVersion
changeList.last().changeListVersion,
synchronizer.getChangeListVersions().authorVersion
)
}
@Test
fun localAuthorsRepository_sync_deletes_items_marked_deleted_on_network() =
runTest {
val networkAuthors = network.getAuthors()
.map(NetworkAuthor::asEntity)
.map(AuthorEntity::asExternalModel)
val deletedItems = networkAuthors
.map(Author::id)
.partition { it % 2 == 0 }
.first
.toSet()
deletedItems.forEach {
network.editCollection(
collectionType = CollectionType.Authors,
id = it,
isDelete = true
)
}
subject.syncWith(synchronizer)
val dbAuthors = authorDao.getAuthorEntitiesStream()
.first()
.map(AuthorEntity::asExternalModel)
Assert.assertEquals(
networkAuthors.map(Author::id) - deletedItems,
dbAuthors.map(Author::id)
)
// After sync version should be updated
Assert.assertEquals(
network.latestChangeListVersion(CollectionType.Authors),
synchronizer.getChangeListVersions().authorVersion
)
}
}

@ -25,12 +25,14 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.model.authorCrossReferences
import com.google.samples.apps.nowinandroid.core.domain.model.authorEntityShells
import com.google.samples.apps.nowinandroid.core.domain.model.episodeEntityShell
import com.google.samples.apps.nowinandroid.core.domain.model.topicCrossReferences
import com.google.samples.apps.nowinandroid.core.domain.model.topicEntityShells
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.CollectionType
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestAuthorDao
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestEpisodeDao
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestNewsResourceDao
@ -39,6 +41,7 @@ import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestTopicDao
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.filteredInterestsIds
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.nonPresentInterestsIds
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 kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
@ -62,7 +65,7 @@ class LocalNewsRepositoryTest {
private lateinit var network: TestNiaNetwork
private lateinit var niaPreferences: NiaPreferences
private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@ -74,8 +77,10 @@ class LocalNewsRepositoryTest {
authorDao = TestAuthorDao()
topicDao = TestTopicDao()
network = TestNiaNetwork()
niaPreferences = NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
synchronizer = TestSynchronizer(
NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
)
)
subject = LocalNewsRepository(
@ -84,7 +89,6 @@ class LocalNewsRepositoryTest {
authorDao = authorDao,
topicDao = topicDao,
network = network,
niaPreferences = niaPreferences,
)
}
@ -151,7 +155,7 @@ class LocalNewsRepositoryTest {
@Test
fun localNewsRepository_sync_pulls_from_network() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
val newsResourcesFromNetwork = network.getNewsResources()
.map(NetworkNewsResource::asEntity)
@ -168,8 +172,47 @@ class LocalNewsRepositoryTest {
// After sync version should be updated
assertEquals(
newsResourcesFromNetwork.lastIndex,
niaPreferences.getChangeListVersions().newsResourceVersion
network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion
)
}
@Test
fun localNewsRepository_sync_deletes_items_marked_deleted_on_network() =
runTest {
val newsResourcesFromNetwork = network.getNewsResources()
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
val deletedItems = newsResourcesFromNetwork
.map(NewsResource::id)
.partition { it % 2 == 0 }
.first
.toSet()
deletedItems.forEach {
network.editCollection(
collectionType = CollectionType.NewsResources,
id = it,
isDelete = true
)
}
subject.syncWith(synchronizer)
val newsResourcesFromDb = newsResourceDao.getNewsResourcesStream()
.first()
.map(PopulatedNewsResource::asExternalModel)
assertEquals(
newsResourcesFromNetwork.map(NewsResource::id) - deletedItems,
newsResourcesFromDb.map(NewsResource::id)
)
// After sync version should be updated
assertEquals(
network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion
)
}
@ -177,17 +220,24 @@ class LocalNewsRepositoryTest {
fun localNewsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set news version to 7
niaPreferences.updateChangeListVersion {
synchronizer.updateChangeListVersions {
copy(newsResourceVersion = 7)
}
subject.sync()
subject.syncWith(synchronizer)
val changeList = network.changeListsAfter(
CollectionType.NewsResources,
version = 7
)
val changeListIds = changeList
.map(NetworkChangeList::id)
.toSet()
val newsResourcesFromNetwork = network.getNewsResources()
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
// Drop 7 to simulate the first 7 items being unchanged
.drop(7)
.filter { it.id in changeListIds }
val newsResourcesFromDb = newsResourceDao.getNewsResourcesStream()
.first()
@ -200,15 +250,15 @@ class LocalNewsRepositoryTest {
// After sync version should be updated
assertEquals(
newsResourcesFromNetwork.lastIndex + 7,
niaPreferences.getChangeListVersions().newsResourceVersion
changeList.last().changeListVersion,
synchronizer.getChangeListVersions().newsResourceVersion
)
}
@Test
fun localNewsRepository_sync_saves_shell_topic_entities() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources()
@ -223,7 +273,7 @@ class LocalNewsRepositoryTest {
@Test
fun localNewsRepository_sync_saves_shell_author_entities() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources()
@ -238,7 +288,7 @@ class LocalNewsRepositoryTest {
@Test
fun localNewsRepository_sync_saves_shell_episode_entities() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources()
@ -253,7 +303,7 @@ class LocalNewsRepositoryTest {
@Test
fun localNewsRepository_sync_saves_topic_cross_references() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources()
@ -267,7 +317,7 @@ class LocalNewsRepositoryTest {
@Test
fun localNewsRepository_sync_saves_author_cross_references() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources()

@ -21,10 +21,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.CollectionType
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestNiaNetwork
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestTopicDao
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
@ -40,10 +42,12 @@ class LocalTopicsRepositoryTest {
private lateinit var topicDao: TopicDao
private lateinit var network: NiANetwork
private lateinit var network: TestNiaNetwork
private lateinit var niaPreferences: NiaPreferences
private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@ -54,6 +58,7 @@ class LocalTopicsRepositoryTest {
niaPreferences = NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
)
synchronizer = TestSynchronizer(niaPreferences)
subject = LocalTopicsRepository(
topicDao = topicDao,
@ -88,23 +93,23 @@ class LocalTopicsRepositoryTest {
@Test
fun localTopicsRepository_sync_pulls_from_network() =
runTest {
subject.sync()
subject.syncWith(synchronizer)
val network = network.getTopics()
val networkTopics = network.getTopics()
.map(NetworkTopic::asEntity)
val db = topicDao.getTopicEntitiesStream()
val dbTopics = topicDao.getTopicEntitiesStream()
.first()
Assert.assertEquals(
network.map(TopicEntity::id),
db.map(TopicEntity::id)
networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex,
niaPreferences.getChangeListVersions().topicVersion
network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion
)
}
@ -112,29 +117,68 @@ class LocalTopicsRepositoryTest {
fun localTopicsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set topics version to 10
niaPreferences.updateChangeListVersion {
synchronizer.updateChangeListVersions {
copy(topicVersion = 10)
}
subject.sync()
subject.syncWith(synchronizer)
val network = network.getTopics()
val networkTopics = network.getTopics()
.map(NetworkTopic::asEntity)
// Drop 10 to simulate the first 10 items being unchanged
.drop(10)
val db = topicDao.getTopicEntitiesStream()
val dbTopics = topicDao.getTopicEntitiesStream()
.first()
Assert.assertEquals(
networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion
)
}
@Test
fun localTopicsRepository_sync_deletes_items_marked_deleted_on_network() =
runTest {
val networkTopics = network.getTopics()
.map(NetworkTopic::asEntity)
.map(TopicEntity::asExternalModel)
val deletedItems = networkTopics
.map(Topic::id)
.partition { it % 2 == 0 }
.first
.toSet()
deletedItems.forEach {
network.editCollection(
collectionType = CollectionType.Topics,
id = it,
isDelete = true
)
}
subject.syncWith(synchronizer)
val dbTopics = topicDao.getTopicEntitiesStream()
.first()
.map(TopicEntity::asExternalModel)
Assert.assertEquals(
network.map(TopicEntity::id),
db.map(TopicEntity::id)
networkTopics.map(Topic::id) - deletedItems,
dbTopics.map(Topic::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex + 10,
niaPreferences.getChangeListVersions().topicVersion
network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion
)
}

@ -0,0 +1,35 @@
/*
* Copyright 2022 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.domain.repository
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
/**
* Test synchronizer that delegates to [NiaPreferences]
*/
class TestSynchronizer(
private val niaPreferences: NiaPreferences
) : Synchronizer {
override suspend fun getChangeListVersions(): ChangeListVersions =
niaPreferences.getChangeListVersions()
override suspend fun updateChangeListVersions(
update: ChangeListVersions.() -> ChangeListVersions
) = niaPreferences.updateChangeListVersion(update)
}

@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
/**
* Test double for [AuthorDao]
@ -50,4 +51,11 @@ class TestAuthorDao : AuthorDao {
override suspend fun updateAuthors(entities: List<AuthorEntity>) {
throw NotImplementedError("Unused in tests")
}
override suspend fun deleteAuthors(ids: List<Int>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
}
}
}

@ -22,6 +22,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.PopulatedEpisode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.datetime.Instant
/**
@ -55,6 +56,13 @@ class TestEpisodeDao : EpisodeDao {
override suspend fun updateEpisodes(entities: List<EpisodeEntity>) {
throw NotImplementedError("Unused in tests")
}
override suspend fun deleteEpisodes(ids: List<Int>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
}
}
}
private fun EpisodeEntity.asPopulatedEpisode() = PopulatedEpisode(

@ -28,6 +28,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Vid
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.datetime.Instant
val filteredInterestsIds = setOf(1)
@ -97,6 +98,13 @@ class TestNewsResourceDao : NewsResourceDao {
) {
authorCrossReferences = newsResourceAuthorCrossReferences
}
override suspend fun deleteNewsResources(ids: List<Int>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
}
}
}
private fun NewsResourceEntity.asPopulatedNewsResource() = PopulatedNewsResource(

@ -25,6 +25,13 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
enum class CollectionType {
Topics,
Authors,
Episodes,
NewsResources
}
/**
* Test double for [NiANetwork]
*/
@ -32,46 +39,80 @@ class TestNiaNetwork : NiANetwork {
private val networkJson = Json
override suspend fun getTopics(ids: List<Int>?): List<NetworkTopic> =
private val allTopics =
networkJson.decodeFromString<List<NetworkTopic>>(FakeDataSource.topicsData)
.matchIds(
ids = ids,
idGetter = NetworkTopic::id
)
override suspend fun getAuthors(ids: List<Int>?): List<NetworkAuthor> =
private val allAuthors =
networkJson.decodeFromString<List<NetworkAuthor>>(FakeDataSource.authors)
.matchIds(
ids = ids,
idGetter = NetworkAuthor::id
)
override suspend fun getNewsResources(ids: List<Int>?): List<NetworkNewsResource> =
private val allNewsResources =
networkJson.decodeFromString<List<NetworkNewsResource>>(FakeDataSource.data)
.matchIds(
ids = ids,
idGetter = NetworkNewsResource::id
)
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
getTopics(ids = null).mapToChangeList(
after = after,
private val changeLists: MutableMap<CollectionType, List<NetworkChangeList>> = mutableMapOf(
CollectionType.Topics to allTopics
.mapToChangeList(idGetter = NetworkTopic::id),
CollectionType.Authors to allAuthors
.mapToChangeList(idGetter = NetworkAuthor::id),
CollectionType.Episodes to listOf(),
CollectionType.NewsResources to allNewsResources
.mapToChangeList(idGetter = NetworkNewsResource::id),
)
override suspend fun getTopics(ids: List<Int>?): List<NetworkTopic> =
allTopics.matchIds(
ids = ids,
idGetter = NetworkTopic::id
)
override suspend fun getAuthorChangeList(after: Int?): List<NetworkChangeList> =
getAuthors(ids = null).mapToChangeList(
after = after,
override suspend fun getAuthors(ids: List<Int>?): List<NetworkAuthor> =
allAuthors.matchIds(
ids = ids,
idGetter = NetworkAuthor::id
)
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
getNewsResources(ids = null).mapToChangeList(
after = after,
override suspend fun getNewsResources(ids: List<Int>?): List<NetworkNewsResource> =
allNewsResources.matchIds(
ids = ids,
idGetter = NetworkNewsResource::id
)
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
changeLists.getValue(CollectionType.Topics).after(after)
override suspend fun getAuthorChangeList(after: Int?): List<NetworkChangeList> =
changeLists.getValue(CollectionType.Authors).after(after)
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
changeLists.getValue(CollectionType.NewsResources).after(after)
fun latestChangeListVersion(collectionType: CollectionType) =
changeLists.getValue(collectionType).last().changeListVersion
fun changeListsAfter(collectionType: CollectionType, version: Int) =
changeLists.getValue(collectionType).after(version)
/**
* Edits the change list for the backing [collectionType] for the given [id] mimicking
* the server's change list registry
*/
fun editCollection(collectionType: CollectionType, id: Int, isDelete: Boolean) {
val changeList = changeLists.getValue(collectionType)
val latestVersion = changeList.lastOrNull()?.changeListVersion ?: 0
val change = NetworkChangeList(
id = id,
isDelete = isDelete,
changeListVersion = latestVersion + 1,
)
changeLists[collectionType] = changeList.filterNot { it.id == id } + change
}
}
fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> =
when (version) {
null -> this
else -> this.filter { it.changeListVersion > version }
}
/**
* Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null
*/
@ -88,12 +129,11 @@ private fun <T> List<T>.matchIds(
* [after] simulates which models have changed by excluding items before it
*/
private fun <T> List<T>.mapToChangeList(
after: Int?,
idGetter: (T) -> Int
) = mapIndexed { index, item ->
NetworkChangeList(
id = idGetter(item),
changeListVersion = index,
changeListVersion = index + 1,
isDelete = false,
)
}.drop(after ?: 0)
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
/**
* Test double for [TopicDao]
@ -60,4 +61,11 @@ class TestTopicDao : TopicDao {
override suspend fun updateTopics(entities: List<TopicEntity>) {
throw NotImplementedError("Unused in tests")
}
override suspend fun deleteTopics(ids: List<Int>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
}
}
}

@ -19,11 +19,25 @@ package com.google.samples.apps.nowinandroid.core.network.model
import kotlinx.serialization.Serializable
/**
* Network representation of a change list for a model
* Network representation of a change list for a model.
*
* Change lists are a representation of a server-side map like data structure of model ids to
* metadata about that model. In a single change list, a given model id can only show up once.
*/
@Serializable
data class NetworkChangeList(
/**
* The id of the model that was changed
*/
val id: Int,
/**
* Unique consecutive, monotonically increasing version number in the collection describing
* the relative point of change between models in the collection
*/
val changeListVersion: Int,
/**
* Summarizes the update to the model; whether it was deleted or updated.
* Updates include creations.
*/
val isDelete: Boolean,
)

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.testing.repository
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Author
import kotlinx.coroutines.channels.BufferOverflow
@ -52,7 +53,7 @@ class TestAuthorsRepository : AuthorsRepository {
}
}
override suspend fun sync(): Boolean = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
/**
* A test-only API to allow controlling the list of topics from tests.

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.testing.repository
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -54,5 +55,5 @@ class TestNewsRepository : NewsRepository {
newsResourcesFlow.tryEmit(newsResources)
}
override suspend fun sync(): Boolean = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.testing.repository
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.coroutines.channels.BufferOverflow
@ -69,5 +70,5 @@ class TestTopicsRepository : TopicsRepository {
*/
fun getCurrentFollowedTopics(): Set<Int>? = _followedTopicIds.replayCache.firstOrNull()
override suspend fun sync(): Boolean = true
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -19,7 +19,7 @@ package com.google.samples.apps.nowinandroid.sync.initializers
import android.content.Context
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkManager
import androidx.work.WorkManagerInitializer
import com.google.samples.apps.nowinandroid.sync.workers.SyncWorker
@ -43,11 +43,11 @@ private const val SyncWorkName = "SyncWorkName"
class SyncInitializer : Initializer<Sync> {
override fun create(context: Context): Sync {
WorkManager.getInstance(context).apply {
enqueue(SyncWorker.startUpSyncWork())
enqueueUniquePeriodicWork(
// Run sync on app startup and ensure only one sync worker runs at any time
enqueueUniqueWork(
SyncWorkName,
ExistingPeriodicWorkPolicy.KEEP,
SyncWorker.periodicSyncWork()
ExistingWorkPolicy.REPLACE,
SyncWorker.startUpSyncWork()
)
}

@ -22,8 +22,10 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.Synchronizer
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
@ -31,7 +33,6 @@ import com.google.samples.apps.nowinandroid.sync.initializers.SyncConstraints
import com.google.samples.apps.nowinandroid.sync.initializers.syncForegroundInfo
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@ -44,10 +45,11 @@ import kotlinx.coroutines.coroutineScope
class SyncWorker @AssistedInject constructor(
@Assisted private val appContext: Context,
@Assisted workerParams: WorkerParameters,
private val niaPreferences: NiaPreferences,
private val topicRepository: TopicsRepository,
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
) : CoroutineWorker(appContext, workerParams) {
) : CoroutineWorker(appContext, workerParams), Synchronizer {
override suspend fun getForegroundInfo(): ForegroundInfo =
appContext.syncForegroundInfo()
@ -64,10 +66,14 @@ class SyncWorker @AssistedInject constructor(
else Result.retry()
}
companion object {
private const val SyncInterval = 1L
private val SyncIntervalTimeUnit = TimeUnit.DAYS
override suspend fun getChangeListVersions(): ChangeListVersions =
niaPreferences.getChangeListVersions()
override suspend fun updateChangeListVersions(
update: ChangeListVersions.() -> ChangeListVersions
) = niaPreferences.updateChangeListVersion(update)
companion object {
/**
* Expedited one time work to sync data on app startup
*/
@ -76,16 +82,5 @@ class SyncWorker @AssistedInject constructor(
.setConstraints(SyncConstraints)
.setInputData(SyncWorker::class.delegatedData())
.build()
/**
* Periodic sync work to routinely keep the app up to date
*/
fun periodicSyncWork() = PeriodicWorkRequestBuilder<DelegatingWorker>(
SyncInterval,
SyncIntervalTimeUnit
)
.setConstraints(SyncConstraints)
.setInputData(SyncWorker::class.delegatedData())
.build()
}
}

Loading…
Cancel
Save