diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt index 5844a41edc11..3c02fb296d79 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt @@ -28,16 +28,15 @@ fun DeckPicker.importApkg(apkgPath: String) { val deckPicker = this val col = CollectionHelper.getInstance().getCol(deckPicker.baseContext).newBackend catchingLifecycleScope(this) { - val report = runInBackgroundWithProgress(col, { + val report = col.opWithProgress({ if (it.hasImporting()) { // TODO: show progress in GUI Timber.i("%s", it.importing) } }) { - col.importAnkiPackage(apkgPath) + importAnkiPackage(apkgPath) } showSimpleMessageDialog(summarizeReport(col.tr, report)) - deckPicker.updateDeckList() } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index 8d975b55b5c4..7e02c4181f2e 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.lifecycle.LifecycleOwner import androidx.lifecycle.coroutineScope import anki.collection.Progress import com.ichi2.anki.UIUtils.showSimpleSnackbar +import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.CollectionV16 import kotlinx.coroutines.* import net.ankiweb.rsdroid.Backend @@ -51,6 +52,18 @@ suspend fun runInBackground(block: suspend CoroutineScope.() -> T): T { } } +/** + * Run an operation and notify change subscribers. + * * See the docs in ChangeManager.kt + * */ +suspend fun CollectionV16.op(handler: Any? = null, block: suspend CollectionV16.() -> T): T { + return runInBackground { + block() + }.also { + ChangeManager.notifySubscribers(it, handler) + } +} + suspend fun Backend.withProgress(onProgress: (Progress) -> Unit, block: suspend CoroutineScope.() -> T): T { val backend = this return coroutineScope { @@ -75,6 +88,23 @@ suspend fun runInBackgroundWithProgress( } } +/** + * Run an operation and notify change subscribers, and capture backend progress. + * + * See the docs in ChangeManager.kt + * */ +suspend fun CollectionV16.opWithProgress( + onProgress: (Progress) -> Unit, + handler: Any? = null, + op: suspend CollectionV16.() -> T, +): T = coroutineScope { + backend.withProgress(onProgress) { + this@opWithProgress.op(handler) { + op() + } + } +} + suspend fun monitorProgress(backend: Backend, op: (Progress) -> Unit) { while (true) { val progress = backend.latestProgress() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 098c6ea99ba1..5708a741f423 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -54,6 +54,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import anki.collection.OpChanges import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.GravityEnum import com.afollestad.materialdialogs.MaterialDialog @@ -92,6 +93,7 @@ import com.ichi2.async.CollectionTask.* import com.ichi2.async.Connection.CancellableTaskListener import com.ichi2.async.Connection.ConflictResolution import com.ichi2.compat.CompatHelper.Companion.sdkVersion +import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Collection.CheckDatabaseResult import com.ichi2.libanki.Consts import com.ichi2.libanki.Decks @@ -140,7 +142,15 @@ import kotlin.math.roundToLong * * A custom image as a background can be added: [applyDeckPickerBackground] */ @KotlinCleanup("lots to do") -open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncErrorDialogListener, ImportDialogListener, MediaCheckDialogListener, OnRequestPermissionsResultCallback, CustomStudyListener { +open class DeckPicker : + NavigationDrawerActivity(), + StudyOptionsListener, + SyncErrorDialogListener, + ImportDialogListener, + MediaCheckDialogListener, + OnRequestPermissionsResultCallback, + CustomStudyListener, + ChangeManager.ChangeSubscriber { // Short animation duration from system private var mShortAnimDuration = 0 private var mBackButtonPressedToExit = false @@ -191,6 +201,10 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr private lateinit var mCustomStudyDialogFactory: CustomStudyDialogFactory private lateinit var mContextMenuFactory: DeckPickerContextMenu.Factory + init { + ChangeManager.subscribe(this) + } + // ---------------------------------------------------------------------------- // LISTENERS // ---------------------------------------------------------------------------- @@ -2573,4 +2587,10 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr .withEndAction(endAction) } } + + override fun opExecuted(changes: OpChanges, handler: Any?) { + if (changes.studyQueues && handler !== this) { + updateDeckList() + } + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/ChangeManager.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/ChangeManager.kt new file mode 100644 index 000000000000..f4555e36a9a0 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/ChangeManager.kt @@ -0,0 +1,81 @@ +/*************************************************************************************** + * Copyright (c) 2022 Ankitects Pty Ltd * + * * + * This program is free software; you can redistribute it and/or modify it under * + * the terms of the GNU General Public License as published by the Free Software * + * Foundation; either version 3 of the License, or (at your option) any later * + * version. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY * + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * + * PARTICULAR PURPOSE. See the GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License along with * + * this program. If not, see . * + ****************************************************************************************/ + +/** + * With the Rust backend, operations that modify the collection return a description of changes (OpChanges). + * The UI can subscribe to these changes, so it can update itself when actions have been performed + * (eg, the deck list can check if studyQueues has been updated, and if so, it will redraw the list). + * + * The optional handler argument can be used so that the initiator of an action can tell when a + * OpChanges action was caused by itself. This can be useful when the default change behaviour + * should be ignored, in favour of specific handling (eg the UI wishes to directly update the + * displayed flag, without redrawing the entire review screen). + */ + +// BackendFactory.defaultLegacySchema must be false to use this code. + +package com.ichi2.libanki + +import anki.collection.OpChanges +import anki.collection.OpChangesAfterUndo +import anki.collection.OpChangesWithCount +import anki.collection.OpChangesWithId +import anki.import_export.ImportAnkiPackageResponse +import java.lang.ref.WeakReference + +object ChangeManager { + interface ChangeSubscriber { + /** + * Called after a backend method invoked via col.op() or col.opWithProgress() + * has modified the collection. Subscriber should inspect the changes, and update + * the UI if necessary. + */ + fun opExecuted(changes: OpChanges, handler: Any?) + } + + private val subscribers = mutableListOf>() + + fun subscribe(subscriber: ChangeSubscriber) { + subscribers.add(WeakReference(subscriber)) + } + + private fun notifySubscribers(changes: OpChanges, handler: Any?) { + val expired = mutableListOf>() + for (subscriber in subscribers) { + val ref = subscriber.get() + if (ref == null) { + expired.add(subscriber) + } else { + ref.opExecuted(changes, handler) + } + } + for (item in expired) { + subscribers.remove(item) + } + } + + fun notifySubscribers(changes: T, initiator: Any?) { + val opChanges = when (changes) { + is OpChanges -> changes + is OpChangesWithCount -> changes.changes + is OpChangesWithId -> changes.changes + is OpChangesAfterUndo -> changes.changes + is ImportAnkiPackageResponse -> changes.changes + else -> TODO("unhandled change type") + } + notifySubscribers(opChanges, initiator) + } +}