Skip to content

Commit

Permalink
Clean up syncing logic to be more clear (and possibly even more correct)
Browse files Browse the repository at this point in the history
  • Loading branch information
ccomeaux committed Jul 26, 2023
1 parent 0e1a3e2 commit e35e179
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 36 deletions.
16 changes: 8 additions & 8 deletions app/src/main/java/com/boardgamegeek/db/CollectionDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ class CollectionDao(private val context: Context) {
suspend fun saveItem(
item: CollectionItemEntity,
game: CollectionItemGameEntity,
timestamp: Long,
updatedTimestamp: Long,
includeStats: Boolean = true,
includePrivateInfo: Boolean = true,
isBrief: Boolean = false
Expand All @@ -484,9 +484,9 @@ class CollectionDao(private val context: Context) {
if (candidate.dirtyTimestamp != NOT_DIRTY) {
Timber.i("Local copy of the collection item is dirty, skipping sync.")
} else {
upsertGame(item.gameId, toGameValues(game, includeStats, isBrief, timestamp), isBrief)
upsertGame(item.gameId, toGameValues(game, includeStats, isBrief, updatedTimestamp), isBrief)
internalId = upsertItem(
toCollectionValues(item, includeStats, includePrivateInfo, isBrief, timestamp),
toCollectionValues(item, includeStats, includePrivateInfo, isBrief, updatedTimestamp),
isBrief,
candidate
)
Expand All @@ -504,10 +504,10 @@ class CollectionDao(private val context: Context) {
game: CollectionItemGameEntity,
includeStats: Boolean,
isBrief: Boolean,
timestamp: Long
updatedTimestamp: Long
): ContentValues {
val values = ContentValues()
values.put(Games.Columns.UPDATED_LIST, timestamp)
values.put(Games.Columns.UPDATED_LIST, updatedTimestamp)
values.put(Games.Columns.GAME_ID, game.gameId)
values.put(Games.Columns.GAME_NAME, game.gameName)
values.put(Games.Columns.GAME_SORT_NAME, game.sortName)
Expand Down Expand Up @@ -551,13 +551,13 @@ class CollectionDao(private val context: Context) {
includeStats: Boolean,
includePrivateInfo: Boolean,
isBrief: Boolean,
timestamp: Long
updatedTimestamp: Long
): ContentValues {
val values = ContentValues()
if (!isBrief && includePrivateInfo && includeStats) {
values.put(Collection.Columns.UPDATED, timestamp)
values.put(Collection.Columns.UPDATED, updatedTimestamp)
}
values.put(Collection.Columns.UPDATED_LIST, timestamp)
values.put(Collection.Columns.UPDATED_LIST, updatedTimestamp)
values.put(Collection.Columns.GAME_ID, item.gameId)
if (item.collectionId != INVALID_ID) {
values.put(Collection.Columns.COLLECTION_ID, item.collectionId)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/boardgamegeek/db/GameDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ class GameDao(private val context: Context) {
* Get a list of games, sorted by least recently updated, that
* 1. have no associated collection record
* 2. haven't been viewed in a configurable number of hours
* 3. and have 0 plays (if plays are being synced
* 3. and have 0 plays (if plays are being synced)
*/
suspend fun loadDeletableGames(hoursAgo: Long, includeUnplayedGames: Boolean): List<Pair<Int, String>> = withContext(Dispatchers.IO) {
val games = mutableListOf<Pair<Int, String>>()
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/boardgamegeek/pref/SyncPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ fun SharedPreferences.setPartialCollectionSyncLastCompletedAt(timestamp: Long =
}

fun SharedPreferences.getPartialCollectionSyncLastCompletedAt(subtype: BggService.ThingSubtype?): Long {
val ts = this.getPartialCollectionSyncLastCompletedAt()
return this["${TIMESTAMP_COLLECTION_PARTIAL}.${subtype?.code.orEmpty()}", ts] ?: ts
return this["${TIMESTAMP_COLLECTION_PARTIAL}.${subtype?.code.orEmpty()}", 0L] ?: 0L
}

fun SharedPreferences.setPartialCollectionSyncLastCompletedAt(subtype: BggService.ThingSubtype?, timestamp: Long = System.currentTimeMillis()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class CollectionItemRepository(
private val dao = CollectionDao(context)
private val prefs: SharedPreferences by lazy { context.preferences() }
private val syncPrefs: SharedPreferences by lazy { SyncPrefs.getPrefs(context) }
private val statusesToSync = syncPrefs.getSyncStatusesOrDefault()

suspend fun load(): List<CollectionItemEntity> = dao.load()

Expand Down Expand Up @@ -65,15 +64,15 @@ class CollectionItemRepository(
}
}

suspend fun refresh(options: Map<String, String>, timestamp: Long = System.currentTimeMillis()): Int = withContext(Dispatchers.IO) {
suspend fun refresh(options: Map<String, String>, updatedTimestamp: Long = System.currentTimeMillis()): Int = withContext(Dispatchers.IO) {
var count = 0
val username = prefs[AccountPreferences.KEY_USERNAME, ""]
if (!username.isNullOrBlank()) {
val response = api.collection(username, options)
response.items?.forEach {
val (item, game) = it.mapToEntities()
if (isItemStatusSetToSync(item)) {
val (collectionId, _) = dao.saveItem(item, game, timestamp)
val (collectionId, _) = dao.saveItem(item, game, updatedTimestamp)
if (collectionId != BggContract.INVALID_ID) count++
} else {
Timber.i("Skipped collection item '${item.gameName}' [ID=${item.gameId}, collection ID=${item.collectionId}] - collection status not synced")
Expand All @@ -88,6 +87,7 @@ class CollectionItemRepository(
suspend fun deleteUnupdatedItems(timestamp: Long) = dao.deleteUnupdatedItems(timestamp)

private fun isItemStatusSetToSync(item: CollectionItemEntity): Boolean {
val statusesToSync = syncPrefs.getSyncStatusesOrDefault()
if (item.own && COLLECTION_STATUS_OWN in statusesToSync) return true
if (item.previouslyOwned && COLLECTION_STATUS_PREVIOUSLY_OWNED in statusesToSync) return true
if (item.forTrade && COLLECTION_STATUS_FOR_TRADE in statusesToSync) return true
Expand Down
42 changes: 20 additions & 22 deletions app/src/main/java/com/boardgamegeek/work/SyncCollectionWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ class SyncCollectionWorker @AssistedInject constructor(
return if (syncPrefs.getCurrentCollectionSyncTimestamp() == 0L) {
val fetchIntervalInDays = RemoteConfig.getInt(RemoteConfig.KEY_SYNC_COLLECTION_FETCH_INTERVAL_DAYS)
val lastCompleteSync = syncPrefs.getLastCompleteCollectionTimestamp()
if (lastCompleteSync > 0 && !lastCompleteSync.isOlderThan(fetchIntervalInDays.days)) {
Timber.i("Not currently syncing the complete collection but it's been less than $fetchIntervalInDays days since we synced completely [${lastCompleteSync.toDateTime()}]; syncing recently modified collection instead")
syncCollectionModifiedSince()
} else {
Timber.i("Not currently syncing the complete collection and it's been more than $fetchIntervalInDays days since we synced completely [${lastCompleteSync.toDateTime()}]; syncing entire collection")
if (lastCompleteSync == 0L || lastCompleteSync.isOlderThan(fetchIntervalInDays.days)) {
Timber.i("It's been more than $fetchIntervalInDays days since we synced completely [${lastCompleteSync.toDateTime()}]; syncing entire collection")
syncPrefs.setCurrentCollectionSyncTimestamp()
syncCompleteCollection()
} else {
Timber.i("It's been less than $fetchIntervalInDays days since we synced completely [${lastCompleteSync.toDateTime()}]; syncing recently modified collection instead")
syncCollectionModifiedSince()
}
} else {
Timber.i("Continuing an in-progress sync of entire collection")
Expand Down Expand Up @@ -94,7 +94,6 @@ class SyncCollectionWorker @AssistedInject constructor(
deleteUnusedItems()

syncPrefs.setLastCompleteCollectionTimestamp(syncPrefs.getCurrentCollectionSyncTimestamp())
syncPrefs.setPartialCollectionSyncLastCompletedAt(syncPrefs.getCurrentCollectionSyncTimestamp())
syncPrefs.setCurrentCollectionSyncTimestamp(0L)

Timber.i("Complete collection synced successfully")
Expand All @@ -117,22 +116,22 @@ class SyncCollectionWorker @AssistedInject constructor(
val contentText = applicationContext.getString(R.string.sync_notification_collection_detail, statusDescription, subtypeDescription)
setForeground(createForegroundInfo(contentText))

val timestamp = System.currentTimeMillis()
val result = performSync(timestamp, subtype, null, status, excludedStatuses, errorMessage = contentText)
if (result is Result.Success) syncPrefs.setCompleteCollectionSyncTimestamp(subtype, status, timestamp)
val updatedTimestamp = System.currentTimeMillis()
val result = performSync(updatedTimestamp, subtype, null, status, excludedStatuses, errorMessage = contentText)
if (result is Result.Success) syncPrefs.setCompleteCollectionSyncTimestamp(subtype, status, updatedTimestamp)
return result
}

private suspend fun syncCollectionModifiedSince(): Result {
Timber.i("Starting to sync recently modified collection")
try {
val itemResult = syncBySubtype(null)
val itemResult = syncCollectionModifiedSinceBySubtype(null)
if (itemResult is Result.Failure) return itemResult

if (isStopped) return Result.failure(workDataOf(STOPPED_REASON to "Sync stopped before recently modified accessories, aborting"))
delay(RemoteConfig.getLong(RemoteConfig.KEY_SYNC_COLLECTION_FETCH_PAUSE_MILLIS))

val accessoryResult = syncBySubtype(BggService.ThingSubtype.BOARDGAME_ACCESSORY)
val accessoryResult = syncCollectionModifiedSinceBySubtype(BggService.ThingSubtype.BOARDGAME_ACCESSORY)
if (accessoryResult is Result.Failure) return accessoryResult

syncPrefs.setPartialCollectionSyncLastCompletedAt()
Expand All @@ -143,21 +142,20 @@ class SyncCollectionWorker @AssistedInject constructor(
}
}

private suspend fun syncBySubtype(subtype: BggService.ThingSubtype?): Result {
private suspend fun syncCollectionModifiedSinceBySubtype(subtype: BggService.ThingSubtype?): Result {
Timber.i("Starting to sync subtype [${subtype?.code ?: "<none>"}]")
val lastStatusSync = syncPrefs.getPartialCollectionSyncLastCompletedAt(subtype)
val lastPartialSync = syncPrefs.getPartialCollectionSyncLastCompletedAt()
if (lastStatusSync > lastPartialSync) {
val previousSyncTimestamp = syncPrefs.getPartialCollectionSyncLastCompletedAt(subtype)
if (previousSyncTimestamp > syncPrefs.getPartialCollectionSyncLastCompletedAt()) {
Timber.i("Subtype [${subtype?.code ?: "<none>"}] has been synced in the current sync request; aborting")
return Result.success()
}

val contentText = applicationContext.getString(R.string.sync_notification_collection_since, subtype.getDescription(applicationContext), lastStatusSync.toDateTime())
val contentText = applicationContext.getString(R.string.sync_notification_collection_since, subtype.getDescription(applicationContext), previousSyncTimestamp.toDateTime())
setForeground(createForegroundInfo(contentText))

val timestamp = System.currentTimeMillis()
val result = performSync(timestamp, subtype, lastStatusSync, errorMessage = contentText)
if (result is Result.Success) syncPrefs.setPartialCollectionSyncLastCompletedAt(subtype, timestamp)
val updatedTimestamp = System.currentTimeMillis()
val result = performSync(updatedTimestamp, subtype, previousSyncTimestamp, errorMessage = contentText)
if (result is Result.Success) syncPrefs.setPartialCollectionSyncLastCompletedAt(subtype, updatedTimestamp)
return result
}

Expand Down Expand Up @@ -195,7 +193,7 @@ class SyncCollectionWorker @AssistedInject constructor(
}

private suspend fun performSync(
timestamp: Long = System.currentTimeMillis(),
updatedTimestamp: Long = System.currentTimeMillis(),
subtype: BggService.ThingSubtype? = null,
sinceTimestamp: Long? = null,
status: String? = null,
Expand All @@ -216,7 +214,7 @@ class SyncCollectionWorker @AssistedInject constructor(
gameIds?.let { options[BggService.COLLECTION_QUERY_KEY_ID] = it.joinToString(",") }

val result = try {
val count = collectionItemRepository.refresh(options, timestamp)
val count = collectionItemRepository.refresh(options, updatedTimestamp)
Timber.i(
"Saved $count collection ${subtype.getDescription(applicationContext).lowercase()}" +
if (status != null) " of status $status" else "" +
Expand Down Expand Up @@ -252,7 +250,7 @@ class SyncCollectionWorker @AssistedInject constructor(
hoursAgo,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NUMERIC_DATE or DateUtils.FORMAT_SHOW_TIME
)
Timber.i("Fetching games that aren't in the collection and have not been viewed since $date")
Timber.i("Finding games to delete that aren't in the collection and have not been viewed since $date")

val games = gameRepository.loadDeletableGames(hoursAgo, prefs.isStatusSetToSync(COLLECTION_STATUS_PLAYED))
if (games.isNotEmpty()) {
Expand Down

0 comments on commit e35e179

Please sign in to comment.