From 1fb870ba2587a704613f4e88112ad964e8c94f60 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 25 Jul 2022 09:28:17 +1000 Subject: [PATCH] Require V1 scheduler users to update when they try to study Studying with the V1 scheduler enabled is no longer possible on recent Anki, AnkiMobile or AnkiWeb versions, so AnkiDroid is the last one still supporting it. Pushing users to upgrade will save them from some of the footguns V1 had, and will allow AnkiDroid to cut out some code and tests. Many users have likely already upgraded due to the use of the other clients. This commit adds support for the backend upgrade code, so that learning cards will not be reset on upgrade. To make use of this, users will be automatically updated to the latest schema version, the scheduler upgrade will be performed, and then they're moved back to the legacy schema. I've also added a helper to more ergonomically deal with schema changes. --- .../java/com/ichi2/anki/CoroutineHelpers.kt | 29 ++++++++- .../main/java/com/ichi2/anki/DeckPicker.kt | 61 +++++++++++++++++++ .../main/java/com/ichi2/libanki/Collection.kt | 2 +- .../com/ichi2/libanki/sched/BackendSched.kt | 7 +++ AnkiDroid/src/main/res/values/03-dialogs.xml | 1 + 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index a46c3db6dc07..70c5b5f6c6eb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -21,6 +21,7 @@ import androidx.appcompat.app.AlertDialog import androidx.lifecycle.coroutineScope import anki.collection.Progress import com.ichi2.anki.UIUtils.showSimpleSnackbar +import com.ichi2.libanki.Collection import com.ichi2.libanki.CollectionV16 import com.ichi2.themes.StyledProgressDialog import kotlinx.coroutines.* @@ -28,6 +29,8 @@ import net.ankiweb.rsdroid.Backend import net.ankiweb.rsdroid.BackendException import net.ankiweb.rsdroid.exceptions.BackendInterruptedException import timber.log.Timber +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * Launch a job that catches any uncaught errors and reports them to the user. @@ -127,7 +130,7 @@ suspend fun AnkiActivity.runInBackgroundWithProgress( * window with the provided message. */ suspend fun AnkiActivity.runInBackgroundWithProgress( - message: String = "", + message: String = resources.getString(R.string.dialog_processing), op: suspend () -> T ): T = withProgressDialog( context = this@runInBackgroundWithProgress, @@ -205,3 +208,27 @@ private fun ProgressContext.updateDialog(dialog: android.app.ProgressDialog) { @Suppress("Deprecation") // ProgressDialog deprecation dialog.setMessage(text + progressText) } + +/** + * If a full sync is not already required, confirm the user wishes to proceed. + * If the user agrees, the schema is bumped and the routine will return true. + * On false, calling routine should abort. + */ +suspend fun AnkiActivity.userAcceptsSchemaChange(col: Collection): Boolean { + if (col.schemaChanged()) { + return true + } + return suspendCoroutine { coroutine -> + AlertDialog.Builder(this) + // generic message + .setMessage(col.tr.deckConfigWillRequireFullSync()) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + col.modSchemaNoCheck() + coroutine.resume(true) + } + .setNegativeButton(R.string.dialog_cancel) { _, _ -> + coroutine.resume(false) + } + .show() + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 89d6dbe96ad4..04ec0bba12de 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -42,6 +42,7 @@ import android.view.WindowManager.BadTokenException import android.widget.* import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback @@ -95,6 +96,7 @@ import com.ichi2.libanki.Collection.CheckDatabaseResult import com.ichi2.libanki.importer.AnkiPackageImporter import com.ichi2.libanki.sched.AbstractDeckTreeNode import com.ichi2.libanki.sched.TreeNode +import com.ichi2.libanki.sched.upgradeScheduler import com.ichi2.libanki.sync.CustomSyncServerUrlException import com.ichi2.libanki.sync.Syncer.ConnectionResultType import com.ichi2.libanki.utils.TimeManager @@ -1950,11 +1952,41 @@ open class DeckPicker : } } + private fun promptUserToUpdateScheduler() { + val builder = AlertDialog.Builder(this) + .setMessage(col.tr.schedulingUpdateRequired()) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + launchCatchingTask { + if (!userAcceptsSchemaChange(col)) { + return@launchCatchingTask + } + runInBackgroundWithProgress { + CollectionHelper.getInstance().updateScheduler(this@DeckPicker) + } + showThemedToast(this@DeckPicker, col.tr.schedulingUpdateDone(), false) + refreshState() + } + } + .setNegativeButton(R.string.dialog_cancel) { _, _ -> + // nothing to do + } + if (AdaptionUtil.hasWebBrowser(this)) { + builder.setNeutralButton(col.tr.schedulingUpdateMoreInfoButton()) { _, _ -> + this.openUrl(Uri.parse("https://faqs.ankiweb.net/the-anki-2.1-scheduler.html#updating")) + } + } + builder.show() + } + private fun handleDeckSelection(did: Long, selectionType: DeckSelectionType) { // Clear the undo history when selecting a new deck if (col.decks.selected() != did) { col.clearUndo() } + if (col.get_config_int("schedVer") == 1) { + promptUserToUpdateScheduler() + return + } // Select the deck col.decks.select(did) // Also forget the last deck used by the Browser @@ -2655,3 +2687,32 @@ open class DeckPicker : } } } + +/** Upgrade from v1 to v2 scheduler. + * Caller must have confirmed schema modification already. + */ +@KotlinCleanup("move into CollectionHelper once it's converted to Kotlin") +@Synchronized +fun CollectionHelper.updateScheduler(context: Context) { + if (BackendFactory.defaultLegacySchema) { + // We'll need to temporarily update to the latest schema. + closeCollection(true, "sched upgrade") + discardBackend() + BackendFactory.defaultLegacySchema = false + // Ensure collection closed if upgrade fails, and schema reverted + // even if close fails. + try { + try { + getCol(context).newBackend.upgradeScheduler() + } finally { + closeCollection(true, "sched upgrade") + } + } finally { + BackendFactory.defaultLegacySchema = true + discardBackend() + } + } else { + // Can upgrade directly + getCol(context).newBackend.upgradeScheduler() + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt index 18361eee8a04..f74f7f9e5064 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt @@ -248,7 +248,7 @@ open class Collection( } // Note: Additional members in the class duplicate this - private fun _loadScheduler() { + fun _loadScheduler() { val ver = schedVer() if (ver == 1) { sched = Sched(this) diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/BackendSched.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/BackendSched.kt index 03970bd65513..e58b4fa80122 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/BackendSched.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/BackendSched.kt @@ -55,3 +55,10 @@ fun CollectionV16.deckTreeLegacy(includeCounts: Boolean): ListOK No Continue + Processing... Create Delete