Skip to content

Commit

Permalink
feat: sync collection in the background
Browse files Browse the repository at this point in the history
aimed only to be used with autosync, so the user can leave the app without having to wait the synchronization to be complete.

I combined the SyncMediaWorker into it to use the same notification and reduce noise
  • Loading branch information
BrayanDSO committed Mar 27, 2024
1 parent cfe141b commit 56c2d69
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 181 deletions.
20 changes: 14 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.dialogs.storageMigrationFailedDialogIsShownOrPending
import com.ichi2.anki.utils.SECONDS_PER_DAY
import com.ichi2.anki.widgets.DeckAdapter
import com.ichi2.anki.worker.SyncMediaWorker
import com.ichi2.anki.worker.SyncWorker
import com.ichi2.annotations.NeedsTest
import com.ichi2.async.*
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
Expand Down Expand Up @@ -1202,7 +1202,7 @@ open class DeckPicker :
}
}

private suspend fun automaticSync() {
private suspend fun automaticSync(runInBackground: Boolean = false) {
Timber.v("automaticSync()")

/**
Expand Down Expand Up @@ -1247,12 +1247,18 @@ open class DeckPicker :
Timber.d("autoSync: no collection changes to sync. Syncing media if set")
if (shouldFetchMedia(sharedPrefs())) {
val auth = syncAuth() ?: return
SyncMediaWorker.start(this, auth)
SyncWorker.startMediaSync(this, auth)
}
}
else -> {
Timber.i("Triggering Automatic Sync")
sync()
if (runInBackground) {
Timber.i("Triggering background automatic sync")
val auth = syncAuth() ?: return
SyncWorker.startCollectionSync(this, auth, shouldFetchMedia(sharedPrefs()))
} else {
Timber.i("Triggering foreground automatic sync")
sync()
}
}
}
}
Expand All @@ -1272,8 +1278,10 @@ open class DeckPicker :
false
) || backButtonPressedToExit
) {
// can't use launchCatchingTask because any errors
// would need to be shown in the UI
lifecycleScope.launch {
automaticSync()
automaticSync(runInBackground = true)
}.invokeOnCompletion {
finish()
}
Expand Down
10 changes: 5 additions & 5 deletions AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import com.ichi2.anki.dialogs.SyncErrorDialog
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.servicelayer.ScopedStorageService
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.worker.SyncMediaWorker
import com.ichi2.anki.worker.SyncWorker
import com.ichi2.async.AsyncOperation
import com.ichi2.libanki.createBackup
import com.ichi2.libanki.fullUploadOrDownload
Expand Down Expand Up @@ -208,7 +208,7 @@ private suspend fun handleNormalSync(
manualCancelButton = R.string.dialog_cancel
) {
withCol {
syncCollection(auth2, media = false) // media is synced by SyncMediaWorker
syncCollection(auth2, media = false) // media is synced in the background by SyncWorker
}
}

Expand All @@ -228,7 +228,7 @@ private suspend fun handleNormalSync(
deckPicker.showSyncLogMessage(R.string.sync_database_acknowledge, output.serverMessage)
deckPicker.refreshState()
if (syncMedia) {
SyncMediaWorker.start(deckPicker, auth2)
SyncWorker.startMediaSync(deckPicker, auth2)
}
}

Expand Down Expand Up @@ -286,7 +286,7 @@ private suspend fun handleDownload(
}
deckPicker.refreshState()
if (mediaUsn != null) {
SyncMediaWorker.start(deckPicker, auth)
SyncWorker.startMediaSync(deckPicker, auth)
}
}

Expand All @@ -313,7 +313,7 @@ private suspend fun handleUpload(
}
deckPicker.refreshState()
if (mediaUsn != null) {
SyncMediaWorker.start(deckPicker, auth)
SyncWorker.startMediaSync(deckPicker, auth)
}
}
Timber.i("Full Upload Completed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
package com.ichi2.anki.notifications

object NotificationId {
const val SYNC_MEDIA = 123
const val SYNC = 123
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,28 @@ import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager.getDefaultSharedPreferences
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.R
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.dialogs.ActivityAgnosticDialogs.Companion.MIGRATION_FAILED_DIALOG_ERROR_TEXT_KEY
import com.ichi2.anki.utils.getUserFriendlyErrorText
import com.ichi2.anki.withProgress
import com.ichi2.anki.worker.UniqueWorkNames
import com.ichi2.utils.copyToClipboard
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import makeLinksClickable
import timber.log.Timber

// TODO BEFORE-RELEASE Dismiss the related notification, if any, when the dialog is dismissed.
// Currently we are leaving the notification dangling after the migration has completed.
Expand Down Expand Up @@ -183,6 +198,50 @@ class ActivityAgnosticDialogs private constructor(private val application: Appli
}
}
})

// Shows a progress dialog if the user tries to open the app
// while the collection is being synced in the background
// COULD_BE_BETTER: get the progress directly from the Worker instead of
// retrieving it again from the backend
application.registerActivityLifecycleCallbacks(object : DefaultActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
if (activity !is FragmentActivity) return

val workManager = WorkManager.getInstance(activity)
val workInfoFlow = workManager.getWorkInfosForUniqueWorkFlow(UniqueWorkNames.SYNC)
val backend = CollectionManager.getBackend() // just for checking progress

workInfoFlow.flowWithLifecycle(activity.lifecycle).onEach { workInfoList ->
val workInfo = workInfoList.firstOrNull()
Timber.v("Sync last work: $workInfo")

if (workInfo?.state == WorkInfo.State.RUNNING) {
val progress = withContext(Dispatchers.IO) {
backend.latestProgress()
}
if (!progress.hasNormalSync() || progress.hasMediaSync()) {
return@onEach
}

Timber.d("Showing background sync dialog")
activity.withProgress(extractProgress = {
if (progress.hasNormalSync()) {
text = progress.normalSync.run { "$added\n$removed" }
}
}, onCancel = {
workManager.cancelWorkById(workInfo.id)
activity.showSnackbar(R.string.sync_cancelled, Snackbar.LENGTH_SHORT)
}, manualCancelButton = R.string.dialog_cancel) {
withContext(Dispatchers.IO) {
while (backend.latestProgress().hasNormalSync()) {
delay(100)
}
}
}
}
}.launchIn(activity.lifecycleScope)
}
})
}

companion object {
Expand Down
168 changes: 0 additions & 168 deletions AnkiDroid/src/main/java/com/ichi2/anki/worker/SyncMediaWorker.kt

This file was deleted.

Loading

0 comments on commit 56c2d69

Please sign in to comment.