Skip to content

Commit

Permalink
Add ChangeManager
Browse files Browse the repository at this point in the history
  • Loading branch information
dae committed Jun 25, 2022
1 parent ecaec8d commit 129dfee
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 4 deletions.
5 changes: 2 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
30 changes: 30 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +52,18 @@ suspend fun <T> runInBackground(block: suspend CoroutineScope.() -> T): T {
}
}

/**
* Run an operation and notify change subscribers.
* * See the docs in ChangeManager.kt
* */
suspend fun <T> CollectionV16.op(handler: Any? = null, block: suspend CollectionV16.() -> T): T {
return runInBackground {
block()
}.also {
ChangeManager.notifySubscribers(it, handler)
}
}

suspend fun <T> Backend.withProgress(onProgress: (Progress) -> Unit, block: suspend CoroutineScope.() -> T): T {
val backend = this
return coroutineScope {
Expand All @@ -75,6 +88,23 @@ suspend fun <T> runInBackgroundWithProgress(
}
}

/**
* Run an operation and notify change subscribers, and capture backend progress.
*
* See the docs in ChangeManager.kt
* */
suspend fun <T> 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()
Expand Down
22 changes: 21 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -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()
}
}
}
81 changes: 81 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/libanki/ChangeManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/***************************************************************************************
* Copyright (c) 2022 Ankitects Pty Ltd <http://apps.ankiweb.net> *
* *
* 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 <http://www.gnu.org/licenses/>. *
****************************************************************************************/

/**
* 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<WeakReference<ChangeSubscriber>>()

fun subscribe(subscriber: ChangeSubscriber) {
subscribers.add(WeakReference(subscriber))
}

private fun notifySubscribers(changes: OpChanges, handler: Any?) {
val expired = mutableListOf<WeakReference<ChangeSubscriber>>()
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<T> 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)
}
}

0 comments on commit 129dfee

Please sign in to comment.