From 31043d77bec95f8ca7f03d7954a9c77bdfe78085 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:13:54 +0000 Subject: [PATCH 1/5] docs(decks): document 'remove' return value --- AnkiDroid/src/main/java/com/ichi2/libanki/Decks.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Decks.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Decks.kt index 3728689fdf55..88c6c49d1bad 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Decks.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Decks.kt @@ -141,7 +141,11 @@ class Decks( type: DeckConfigId = 0L, ): DeckId = id(name = name, create = true, type = type)!! - fun remove(deckIds: Iterable): OpChangesWithCount = col.backend.removeDecks(dids = deckIds) + /** + * Deletes one or more decks from the collection + * @return [OpChangesWithCount]: the number of cards deleted + */ + fun remove(deckIds: Iterable): OpChangesWithCount = col.backend.removeDecks(dids = deckIds) /** A sorted sequence of deck names and IDs. */ @LibAnkiAlias("all_names_and_ids") From 9ba7bbf685fd1365825416c8bad5abf006924aee Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:14:44 +0000 Subject: [PATCH 2/5] refactor: move launchCollectionInLifecycleScope It will also be used in the Deck Picker --- .../main/java/com/ichi2/anki/AnkiActivity.kt | 22 +++++++++++++++++++ .../main/java/com/ichi2/anki/CardBrowser.kt | 19 ---------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index 4068345c0e1f..94c741781401 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -43,6 +43,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.google.android.material.color.MaterialColors import com.google.android.material.snackbar.Snackbar import com.ichi2.anim.ActivityTransitionAnimation @@ -54,6 +55,7 @@ import com.ichi2.anki.android.input.Shortcut import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.ShortcutGroupProvider import com.ichi2.anki.android.input.shortcut +import com.ichi2.anki.common.utils.android.isRobolectric import com.ichi2.anki.dialogs.AsyncDialogFragment import com.ichi2.anki.dialogs.DatabaseErrorDialog import com.ichi2.anki.dialogs.DatabaseErrorDialog.CustomExceptionData @@ -74,7 +76,11 @@ import com.ichi2.compat.customtabs.CustomTabsHelper import com.ichi2.libanki.Collection import com.ichi2.themes.Themes import com.ichi2.utils.AdaptionUtil +import com.ichi2.utils.HandlerUtils import com.ichi2.utils.KotlinCleanup +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import androidx.browser.customtabs.CustomTabsIntent.Builder as CustomTabsIntentBuilder @@ -716,6 +722,22 @@ open class AnkiActivity : return false } + // TODO: Move this to an extension method once we have context parameters + protected fun Flow.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) { + lifecycleScope.launch { + this@launchCollectionInLifecycleScope.collect { + if (isRobolectric) { + // hack: lifecycleScope/runOnUiThread do not handle our + // test dispatcher overriding both IO and Main + // in tests, waitForAsyncTasksToComplete may be required. + HandlerUtils.postOnNewHandler { runBlocking { block(it) } } + } else { + block(it) + } + } + } + } + override val shortcuts get(): ShortcutGroup? = null diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 23eb2c324c9a..fdd6d56c7e57 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -63,7 +63,6 @@ import com.ichi2.anki.browser.SaveSearchResult import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository import com.ichi2.anki.browser.getLabel import com.ichi2.anki.browser.toCardBrowserLaunchOptions -import com.ichi2.anki.common.utils.android.isRobolectric import com.ichi2.anki.dialogs.BrowserOptionsDialog import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog.Companion.newInstance @@ -111,7 +110,6 @@ import com.ichi2.libanki.QueueType import com.ichi2.libanki.SortOrder import com.ichi2.libanki.undoableOp import com.ichi2.ui.CardBrowserSearchView -import com.ichi2.utils.HandlerUtils import com.ichi2.utils.KotlinCleanup import com.ichi2.utils.LanguageUtil import com.ichi2.utils.TagsUtil.getUpdatedTags @@ -119,9 +117,7 @@ import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons import com.ichi2.widget.WidgetStatus.updateInBackground import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import net.ankiweb.rsdroid.RustCleanup import net.ankiweb.rsdroid.Translations @@ -1900,21 +1896,6 @@ open class CardBrowser : viewModel: CardBrowserViewModel, ): Intent = NoteEditorLauncher.AddNoteFromCardBrowser(viewModel).getIntent(context) } - - private fun Flow.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) { - lifecycleScope.launch { - this@launchCollectionInLifecycleScope.collect { - if (isRobolectric) { - // hack: lifecycleScope/runOnUiThread do not handle our - // test dispatcher overriding both IO and Main - // in tests, waitForAsyncTasksToComplete may be required. - HandlerUtils.postOnNewHandler { runBlocking { block(it) } } - } else { - block(it) - } - } - } - } } suspend fun searchForRows( From 2ed7555e5c55e4799f34158072e074190b1acfb3 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:18:57 +0000 Subject: [PATCH 3/5] improvement(deck-picker): introduce ViewModel We want to use a ViewModel for DeckPicker This is the start * extract 'focusedDeck' * will be used in the next commit --- .../main/java/com/ichi2/anki/DeckPicker.kt | 32 ++++++++----------- .../anki/deckpicker/DeckPickerViewModel.kt | 32 +++++++++++++++++++ .../java/com/ichi2/anki/DeckPickerTest.kt | 4 +-- 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 3b47d36e010c..edcd34523550 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -52,6 +52,7 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog @@ -104,6 +105,7 @@ import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.shortcut import com.ichi2.anki.deckpicker.BITMAP_BYTES_PER_PIXEL import com.ichi2.anki.deckpicker.BackgroundImage +import com.ichi2.anki.deckpicker.DeckPickerViewModel import com.ichi2.anki.dialogs.AsyncDialogFragment import com.ichi2.anki.dialogs.BackupPromptDialog import com.ichi2.anki.dialogs.ConfirmationDialog @@ -249,6 +251,8 @@ open class DeckPicker : CsvImportResultLauncherProvider, CollectionPermissionScreenLauncher, ExportDialogsFactoryProvider { + val viewModel: DeckPickerViewModel by viewModels() + // Short animation duration from system private var shortAnimDuration = 0 private var backButtonPressedToExit = false @@ -320,14 +324,6 @@ open class DeckPicker : */ private var syncOnResume = false - /** - * Keep track of which deck was last given focus in the deck list. If we find that this value - * has changed between deck list refreshes, we need to recenter the deck list to the new current - * deck. - */ - @VisibleForTesting - internal var focusedDeck: DeckId = 0 - private var toolbarSearchItem: MenuItem? = null private var toolbarSearchView: AccessibleSearchView? = null private lateinit var customStudyDialogFactory: CustomStudyDialogFactory @@ -1449,7 +1445,7 @@ open class DeckPicker : if (event.isShiftPressed) { // Shortcut: Shift + DEL - Delete deck without confirmation dialog Timber.i("Shift+DEL: Deck deck without confirmation") - deleteDeck(focusedDeck) + deleteDeck(viewModel.focusedDeck) } else { // Shortcut: DEL Timber.i("Delete Deck from keypress") @@ -1464,7 +1460,7 @@ open class DeckPicker : // that is, when it appears in the trailing study option fragment if (fragmented) { Timber.i("Rename Deck from keypress") - renameDeckDialog(focusedDeck) + renameDeckDialog(viewModel.focusedDeck) return true } } @@ -1515,15 +1511,15 @@ open class DeckPicker : val (deckName, totalCards, isFilteredDeck) = withCol { Triple( - decks.name(focusedDeck), - decks.cardCount(focusedDeck, includeSubdecks = true), - decks.isFiltered(focusedDeck), + decks.name(viewModel.focusedDeck), + decks.cardCount(viewModel.focusedDeck, includeSubdecks = true), + decks.isFiltered(viewModel.focusedDeck), ) } val confirmDeleteDeckDialog = DeckPickerConfirmDeleteDeckDialog.newInstance( deckName = deckName, - deckId = focusedDeck, + deckId = viewModel.focusedDeck, totalCards = totalCards, isFilteredDeck = isFilteredDeck, ) @@ -2130,7 +2126,7 @@ open class DeckPicker : withCol { decks.select(did) } // Also forget the last deck used by the Browser CardBrowser.clearLastDeckId() - focusedDeck = did + viewModel.focusedDeck = did val deck = deckListAdapter.getNodeByDid(did) if (deck.hasCardsReadyToStudy()) { openReviewerOrStudyOptions(selectionType) @@ -2308,9 +2304,9 @@ open class DeckPicker : Timber.e(e, "RuntimeException setting time remaining") } val current = withCol { decks.current().optLong("id") } - if (focusedDeck != current) { + if (viewModel.focusedDeck != current) { scrollDecklistToDeck(current) - focusedDeck = current + viewModel.focusedDeck = current } } @@ -2424,7 +2420,7 @@ open class DeckPicker : } // After deletion: decks.current() reverts to Default, necessitating `focusedDeck` // to match and avoid unnecessary scrolls in `renderPage()`. - focusedDeck = Consts.DEFAULT_DECK_ID + viewModel.focusedDeck = Consts.DEFAULT_DECK_ID showSnackbar(TR.browsingCardsDeletedWithDeckname(changes.count, deckName), Snackbar.LENGTH_SHORT) { setAction(R.string.undo) { undo() } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt new file mode 100644 index 000000000000..2915e637b062 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 David Allison + * + * 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 . + */ + +package com.ichi2.anki.deckpicker + +import androidx.lifecycle.ViewModel +import com.ichi2.anki.DeckPicker +import com.ichi2.libanki.DeckId + +/** @see [DeckPicker] */ +class DeckPickerViewModel : ViewModel() { + /** + * Keep track of which deck was last given focus in the deck list. If we find that this value + * has changed between deck list refreshes, we need to recenter the deck list to the new current + * deck. + */ + // TODO: This should later be handled as a Flow + var focusedDeck: DeckId = 0 +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt index c64801b910b7..6957b51c2308 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt @@ -716,7 +716,7 @@ class DeckPickerTest : RobolectricTest() { assertThat("unbury is not visible: deck has no cards", !col.sched.haveBuried()) deckPicker { - assertThat("deck focus is set", focusedDeck, equalTo(emptyDeck)) + assertThat("deck focus is set", viewModel.focusedDeck, equalTo(emptyDeck)) // ACT: open up the Deck Context Menu val deckToClick = @@ -727,7 +727,7 @@ class DeckPickerTest : RobolectricTest() { // ASSERT assertThat("unbury is visible: one card is buried", col.sched.haveBuried()) - assertThat("deck focus has changed", focusedDeck, equalTo(deckWithCards)) + assertThat("deck focus has changed", viewModel.focusedDeck, equalTo(deckWithCards)) } } From 1055323133c94861f374b9f435a8c87d14a59ee0 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:26:37 +0000 Subject: [PATCH 4/5] refactor(deck-picker): deleteDeck => viewModel * inline confirmDeckDeletion * removing unnecessary 'dismiss' calls * extract `deleteDeck` * introduce `DeckDeletionResult` --- .../main/java/com/ichi2/anki/DeckPicker.kt | 50 +++++++------- .../anki/deckpicker/DeckPickerViewModel.kt | 65 +++++++++++++++++++ .../java/com/ichi2/anki/DeckPickerTest.kt | 3 +- .../anki/dialogs/CreateDeckDialogTest.kt | 2 +- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index edcd34523550..ec610f520c66 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -105,6 +105,7 @@ import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.shortcut import com.ichi2.anki.deckpicker.BITMAP_BYTES_PER_PIXEL import com.ichi2.anki.deckpicker.BackgroundImage +import com.ichi2.anki.deckpicker.DeckDeletionResult import com.ichi2.anki.deckpicker.DeckPickerViewModel import com.ichi2.anki.dialogs.AsyncDialogFragment import com.ichi2.anki.dialogs.BackupPromptDialog @@ -168,7 +169,6 @@ import com.ichi2.libanki.Decks import com.ichi2.libanki.MediaCheckResult import com.ichi2.libanki.exception.ConfirmModSchemaException import com.ichi2.libanki.sched.DeckNode -import com.ichi2.libanki.undoableOp import com.ichi2.libanki.utils.TimeManager import com.ichi2.ui.AccessibleSearchView import com.ichi2.ui.BadgeDrawableBuilder @@ -617,6 +617,18 @@ open class DeckPicker : .build(), onReceiveContentListener, ) + + setupFlows() + } + + private fun setupFlows() { + fun onDeckDeleted(result: DeckDeletionResult) { + showSnackbar(result.toHumanReadableString(), Snackbar.LENGTH_SHORT) { + setAction(R.string.undo) { undo() } + } + } + + viewModel.deckDeletedNotification.launchCollectionInLifecycleScope(::onDeckDeleted) } private val onReceiveContentListener = @@ -654,8 +666,10 @@ open class DeckPicker : /* we can only disable the shortcut for now as it is restricted by Google https://issuetracker.google.com/issues/68949561?pli=1#comment4 * if fixed or given free hand to delete the shortcut with the help of API update this method and use the new one */ + // TODO: it feels buggy that this is not called on all deck deletion paths disableDeckAndChildrenShortcuts(deckId) - confirmDeckDeletion(deckId) + dismissAllDialogFragments() + deleteDeck(deckId) } DeckPickerContextMenuOption.DECK_OPTIONS -> { Timber.i("ContextMenu: Open deck options selected") @@ -1150,8 +1164,9 @@ open class DeckPicker : } R.id.action_deck_delete -> { launchCatchingTask { - val targetDeckId = withCol { decks.selected() } - confirmDeckDeletion(targetDeckId) + withProgress(resources.getString(R.string.delete_deck)) { + viewModel.deleteSelectedDeck().join() + } } return true } @@ -2398,31 +2413,14 @@ open class DeckPicker : createDeckDialog.showDialog() } - fun confirmDeckDeletion(did: DeckId): Job { - // No confirmation required, as undoable - dismissAllDialogFragments() - return deleteDeck(did) - } - /** - * Deletes the provided deck, child decks. and all cards inside. - * Use [.confirmDeckDeletion] for a confirmation dialog - * @param did the deck to delete + * Deletes the provided deck, child decks, and all cards inside. + * @param did ID of the deck to delete */ - fun deleteDeck(did: DeckId): Job = + fun deleteDeck(did: DeckId) = launchCatchingTask { - val deckName = withCol { decks.get(did)!!.name } - val changes = - withProgress(resources.getString(R.string.delete_deck)) { - undoableOp { - decks.remove(listOf(did)) - } - } - // After deletion: decks.current() reverts to Default, necessitating `focusedDeck` - // to match and avoid unnecessary scrolls in `renderPage()`. - viewModel.focusedDeck = Consts.DEFAULT_DECK_ID - showSnackbar(TR.browsingCardsDeletedWithDeckname(changes.count, deckName), Snackbar.LENGTH_SHORT) { - setAction(R.string.undo) { undo() } + withProgress(resources.getString(R.string.delete_deck)) { + viewModel.deleteDeck(did).join() } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt index 2915e637b062..654bea70539f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/DeckPickerViewModel.kt @@ -16,12 +16,27 @@ package com.ichi2.anki.deckpicker +import androidx.annotation.CheckResult import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import anki.i18n.GeneratedTranslations +import com.ichi2.anki.CollectionManager.TR +import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.DeckPicker +import com.ichi2.libanki.Consts import com.ichi2.libanki.DeckId +import com.ichi2.libanki.undoableOp +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch /** @see [DeckPicker] */ class DeckPickerViewModel : ViewModel() { + /** + * @see deleteDeck + * @see DeckDeletionResult + */ + val deckDeletedNotification = MutableSharedFlow() + /** * Keep track of which deck was last given focus in the deck list. If we find that this value * has changed between deck list refreshes, we need to recenter the deck list to the new current @@ -29,4 +44,54 @@ class DeckPickerViewModel : ViewModel() { */ // TODO: This should later be handled as a Flow var focusedDeck: DeckId = 0 + + /** + * Deletes the provided deck, child decks. and all cards inside. + * + * This is a slow operation and should be inside `withProgress` + * + * @param did ID of the deck to delete + */ + @CheckResult // This is a slow operation and should be inside `withProgress` + fun deleteDeck(did: DeckId) = + viewModelScope.launch { + val deckName = withCol { decks.get(did)!!.name } + val changes = undoableOp { decks.remove(listOf(did)) } + // After deletion: decks.current() reverts to Default, necessitating `focusedDeck` + // to match and avoid unnecessary scrolls in `renderPage()`. + focusedDeck = Consts.DEFAULT_DECK_ID + + deckDeletedNotification.emit( + DeckDeletionResult(deckName = deckName, cardsDeleted = changes.count), + ) + } + + /** + * Deletes the currently selected deck + * + * This is a slow operation and should be inside `withProgress` + */ + @CheckResult + fun deleteSelectedDeck() = + viewModelScope.launch { + val targetDeckId = withCol { decks.selected() } + deleteDeck(targetDeckId).join() + } +} + +/** Result of [DeckPickerViewModel.deleteDeck] */ +data class DeckDeletionResult( + val deckName: String, + val cardsDeleted: Int, +) { + /** + * @see GeneratedTranslations.browsingCardsDeletedWithDeckname + */ + // TODO: Somewhat questionable meaning: {count} cards deleted from {deck_name}. + @CheckResult + fun toHumanReadableString() = + TR.browsingCardsDeletedWithDeckname( + count = cardsDeleted, + deckName = deckName, + ) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt index 6957b51c2308..1f93632b1bf9 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt @@ -209,8 +209,7 @@ class DeckPickerTest : RobolectricTest() { DeckPicker::class.java, Intent(), ) - deckPicker.confirmDeckDeletion(did) - advanceRobolectricLooperWithSleep() + deckPicker.viewModel.deleteDeck(did).join() assertThat("deck was deleted", col.decks.count(), equalTo(1)) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/CreateDeckDialogTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/CreateDeckDialogTest.kt index 3bf195287cf8..943e61abc2b9 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/CreateDeckDialogTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/CreateDeckDialogTest.kt @@ -196,7 +196,7 @@ class CreateDeckDialogTest : RobolectricTest() { // After the last deck was created, delete a deck if (decksCount() >= 10) { - deckPicker.confirmDeckDeletion(did) + deckPicker.viewModel.deleteDeck(did).join() assertEquals(deckCounter.decrementAndGet(), decksCount()) assertEquals(deckCounter.get(), decksCount()) From b9b20d7d945add9fda4b9bcb71b161d639576122 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Wed, 8 Jan 2025 02:28:25 +0000 Subject: [PATCH 5/5] improvement: use `repeatOnLifecycle` review request --- .../main/java/com/ichi2/anki/AnkiActivity.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index 94c741781401..bc89e7d2a9cf 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -43,7 +43,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.color.MaterialColors import com.google.android.material.snackbar.Snackbar import com.ichi2.anim.ActivityTransitionAnimation @@ -725,14 +727,16 @@ open class AnkiActivity : // TODO: Move this to an extension method once we have context parameters protected fun Flow.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) { lifecycleScope.launch { - this@launchCollectionInLifecycleScope.collect { - if (isRobolectric) { - // hack: lifecycleScope/runOnUiThread do not handle our - // test dispatcher overriding both IO and Main - // in tests, waitForAsyncTasksToComplete may be required. - HandlerUtils.postOnNewHandler { runBlocking { block(it) } } - } else { - block(it) + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + this@launchCollectionInLifecycleScope.collect { + if (isRobolectric) { + // hack: lifecycleScope/runOnUiThread do not handle our + // test dispatcher overriding both IO and Main + // in tests, waitForAsyncTasksToComplete may be required. + HandlerUtils.postOnNewHandler { runBlocking { block(it) } } + } else { + block(it) + } } } }