From 91cebc42b75e9207ae349ecee9ebb13077fcd335 Mon Sep 17 00:00:00 2001 From: mixelas Date: Mon, 11 May 2026 18:17:36 +0300 Subject: [PATCH 1/5] fix queue import dialogs --- .../com/ichi2/anki/dialogs/ImportDialog.kt | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt index 681e75d6730b..eec522c5e4ce 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt @@ -21,9 +21,11 @@ import androidx.annotation.CheckResult import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import com.ichi2.anki.R +import com.ichi2.anki.dialogs.DialogHandler import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM import com.ichi2.anki.utils.ext.dismissAllDialogFragments +import com.ichi2.utils.ImportUtils import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton import timber.log.Timber @@ -54,8 +56,17 @@ class ImportDialog : AsyncDialogFragment() { .setTitle(R.string.import_title) .setMessage(res().getString(R.string.import_dialog_message_add, displayFileName)) .positiveButton(R.string.import_message_add) { - (activity as ImportDialogListener).importAdd(packagePath) - activity?.dismissAllDialogFragments() + // Try to handle directly for backward compatibility with activities that implement ImportDialogListener + val a = activity + if (a is ImportDialogListener) { + a.importAdd(packagePath) + a.dismissAllDialogFragments() + } else { + // If activity doesn't implement the interface, store a persistent message + // so the DeckPicker can replay it via DialogHandler + DialogHandler.storeMessage(ImportUtils.CollectionImportAdd(packagePath).toMessage()) + activity?.dismissAllDialogFragments() + } }.negativeButton(R.string.dialog_cancel) .create() } @@ -64,8 +75,17 @@ class ImportDialog : AsyncDialogFragment() { .setTitle(R.string.import_title) .setMessage(res().getString(R.string.import_message_replace_confirm, displayFileName)) .positiveButton(R.string.dialog_positive_replace) { - (activity as ImportDialogListener).importReplace(packagePath) - activity?.dismissAllDialogFragments() + // Try to handle directly for backward compatibility with activities that implement ImportDialogListener + val a = activity + if (a is ImportDialogListener) { + a.importReplace(packagePath) + a.dismissAllDialogFragments() + } else { + // If activity doesn't implement the interface, store a persistent message + // so the DeckPicker can replay it via DialogHandler + DialogHandler.storeMessage(ImportUtils.CollectionImportReplace(packagePath).toMessage()) + activity?.dismissAllDialogFragments() + } }.negativeButton(R.string.dialog_cancel) .create() } From 955712084b1672778047de78a7633ba132fa5cee Mon Sep 17 00:00:00 2001 From: mixelas Date: Mon, 11 May 2026 21:26:47 +0300 Subject: [PATCH 2/5] fix: replay import confirmation in DeckPicker --- .../main/java/com/ichi2/utils/ImportUtils.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt index 811b6efd19e6..e4b5d10c9f72 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt @@ -29,6 +29,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import com.ichi2.anki.AnkiActivity import com.ichi2.anki.AnkiDroidApp +import com.ichi2.anki.DeckPicker import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.common.coroutines.applicationScope @@ -512,8 +513,13 @@ object ImportUtils { analyticName = "ImportReplaceDialog", ) { override fun handleAsyncMessage(activity: AnkiActivity) { - // Handle import of collection package APKG - activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM, importPath) + // Only DeckPicker should show import confirmation dialogs. + // If another activity is resumed, keep this message queued until DeckPicker resumes. + if (activity is DeckPicker) { + activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM, importPath) + } else { + DialogHandler.storeMessage(toMessage()) + } } override fun toMessage(): Message = @@ -535,8 +541,13 @@ object ImportUtils { "ImportAddDialog", ) { override fun handleAsyncMessage(activity: AnkiActivity) { - // Handle import of deck package APKG - activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM, importPath) + // Only DeckPicker should show import confirmation dialogs. + // If another activity is resumed, keep this message queued until DeckPicker resumes. + if (activity is DeckPicker) { + activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM, importPath) + } else { + DialogHandler.storeMessage(toMessage()) + } } override fun toMessage(): Message = From d24f09a9fc6b54967ee05a8f86ada3d8e1c1bd40 Mon Sep 17 00:00:00 2001 From: mixelas Date: Tue, 12 May 2026 13:13:00 +0300 Subject: [PATCH 3/5] fix:consolidate import dialog handling --- .../main/java/com/ichi2/anki/DeckPicker.kt | 19 +++++++++ .../com/ichi2/anki/dialogs/ImportDialog.kt | 20 ++++++---- .../com/ichi2/anki/dialogs/ImportViewModel.kt | 39 +++++++++++++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 823542920d72..a4243ec2558b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -129,6 +129,7 @@ import com.ichi2.anki.dialogs.FatalErrorDialog import com.ichi2.anki.dialogs.ImportDialog.ImportDialogListener import com.ichi2.anki.dialogs.ImportFileSelectionFragment.ApkgImportResultLauncherProvider import com.ichi2.anki.dialogs.ImportFileSelectionFragment.CsvImportResultLauncherProvider +import com.ichi2.anki.dialogs.ImportViewModel import com.ichi2.anki.dialogs.SchedulerUpgradeDialog import com.ichi2.anki.dialogs.SyncErrorDialog import com.ichi2.anki.dialogs.SyncErrorDialog.Companion.newInstance @@ -246,6 +247,7 @@ open class DeckPicker : CsvImportResultLauncherProvider, CollectionPermissionScreenLauncher { val viewModel: DeckPickerViewModel by viewModels() + private val importViewModel: ImportViewModel by viewModels() private lateinit var binding: ActivityHomescreenBinding @@ -533,6 +535,23 @@ open class DeckPicker : lifecycleScope.launch { applyDeckPickerBackground() } + // Observe import events emitted by ImportDialog via ImportViewModel. + lifecycleScope.launch { + importViewModel.importAddFlow + .flowWithLifecycle(lifecycle) + .collectLatest { path -> + importAdd(path) + } + } + + lifecycleScope.launch { + importViewModel.importReplaceFlow + .flowWithLifecycle(lifecycle) + .collectLatest { path -> + importReplace(path) + } + } + pullToSyncWrapper = deckPickerBinding.pullToSyncWrapper.apply { setDistanceToTriggerSync(SWIPE_TO_SYNC_TRIGGER_DISTANCE) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt index eec522c5e4ce..3508cbc71751 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt @@ -20,10 +20,11 @@ import android.os.Bundle import androidx.annotation.CheckResult import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf +import androidx.lifecycle.ViewModelProvider import com.ichi2.anki.R -import com.ichi2.anki.dialogs.DialogHandler import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM +import com.ichi2.anki.dialogs.ImportViewModel import com.ichi2.anki.utils.ext.dismissAllDialogFragments import com.ichi2.utils.ImportUtils import com.ichi2.utils.negativeButton @@ -44,6 +45,13 @@ class ImportDialog : AsyncDialogFragment() { private val packagePath: String get() = requireArguments().getString(IMPORT_DIALOG_PACKAGE_PATH_KEY)!! + private lateinit var importViewModel: ImportViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + importViewModel = ViewModelProvider(requireActivity()).get(ImportViewModel::class.java) + } + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { super.onCreate(savedInstanceState) val dialog = AlertDialog.Builder(requireActivity()) @@ -62,9 +70,8 @@ class ImportDialog : AsyncDialogFragment() { a.importAdd(packagePath) a.dismissAllDialogFragments() } else { - // If activity doesn't implement the interface, store a persistent message - // so the DeckPicker can replay it via DialogHandler - DialogHandler.storeMessage(ImportUtils.CollectionImportAdd(packagePath).toMessage()) + // If activity doesn't implement the interface, emit ViewModel event + importViewModel.triggerImportAdd(packagePath) activity?.dismissAllDialogFragments() } }.negativeButton(R.string.dialog_cancel) @@ -81,9 +88,8 @@ class ImportDialog : AsyncDialogFragment() { a.importReplace(packagePath) a.dismissAllDialogFragments() } else { - // If activity doesn't implement the interface, store a persistent message - // so the DeckPicker can replay it via DialogHandler - DialogHandler.storeMessage(ImportUtils.CollectionImportReplace(packagePath).toMessage()) + // If activity doesn't implement the interface, emit ViewModel event + importViewModel.triggerImportReplace(packagePath) activity?.dismissAllDialogFragments() } }.negativeButton(R.string.dialog_cancel) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt new file mode 100644 index 000000000000..848b1c0b0790 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Georgios Michelakis . + */ + +package com.ichi2.anki.dialogs + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class ImportViewModel : ViewModel() { + private val _importAddFlow = MutableSharedFlow() + val importAddFlow = _importAddFlow.asSharedFlow() + + private val _importReplaceFlow = MutableSharedFlow() + val importReplaceFlow = _importReplaceFlow.asSharedFlow() + + fun triggerImportAdd(path: String) { + viewModelScope.launch { _importAddFlow.emit(path) } + } + + fun triggerImportReplace(path: String) { + viewModelScope.launch { _importReplaceFlow.emit(path) } + } +} From cb9a2642dfb3d1388c4a622a7ab7d6b8e659232e Mon Sep 17 00:00:00 2001 From: mixelas Date: Tue, 12 May 2026 15:47:22 +0300 Subject: [PATCH 4/5] fix:apply import dialog handling --- .../main/java/com/ichi2/anki/DeckPicker.kt | 34 ++++--- .../com/ichi2/anki/dialogs/DialogHandler.kt | 5 - .../com/ichi2/anki/dialogs/ImportDialog.kt | 35 +++---- .../com/ichi2/anki/dialogs/ImportViewModel.kt | 30 +++--- .../main/java/com/ichi2/utils/ImportUtils.kt | 94 +++++-------------- 5 files changed, 75 insertions(+), 123 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index a4243ec2558b..23f97a914869 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -65,6 +65,7 @@ import androidx.core.view.isVisible import androidx.draganddrop.DropHelper import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.commit +import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -535,20 +536,27 @@ open class DeckPicker : lifecycleScope.launch { applyDeckPickerBackground() } - // Observe import events emitted by ImportDialog via ImportViewModel. + // Observe import requests emitted by ImportDialog via ImportViewModel. lifecycleScope.launch { - importViewModel.importAddFlow - .flowWithLifecycle(lifecycle) - .collectLatest { path -> - importAdd(path) - } - } - - lifecycleScope.launch { - importViewModel.importReplaceFlow - .flowWithLifecycle(lifecycle) - .collectLatest { path -> - importReplace(path) + importViewModel.pendingImportRequest + .filterNotNull() + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .collectLatest { request -> + runCatching { + showImportDialog(request.dialogType, request.importPath) + }.onFailure { exception -> + if (exception is IllegalStateException) { + showSimpleNotification( + getString(R.string.import_title), + getString(R.string.import_interrupted), + Channel.GENERAL, + ) + } else { + throw exception + } + }.onSuccess { + importViewModel.clearImportRequest() + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DialogHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DialogHandler.kt index 70c973fe2d17..db18ae4693b3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DialogHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DialogHandler.kt @@ -29,7 +29,6 @@ import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.dialogs.DialogHandler.Companion.storeMessage import com.ichi2.anki.showError import com.ichi2.utils.HandlerUtils.getDefaultLooper -import com.ichi2.utils.ImportUtils import timber.log.Timber import java.lang.ref.WeakReference @@ -119,8 +118,6 @@ abstract class DialogHandlerMessage protected constructor( fun fromMessage(message: Message): DialogHandlerMessage = when (WhichDialogHandler.fromInt(message.what)) { WhichDialogHandler.MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG -> CollectionLoadingErrorDialog() - WhichDialogHandler.MSG_SHOW_COLLECTION_IMPORT_REPLACE_DIALOG -> ImportUtils.CollectionImportReplace.fromMessage(message) - WhichDialogHandler.MSG_SHOW_COLLECTION_IMPORT_ADD_DIALOG -> ImportUtils.CollectionImportAdd.fromMessage(message) WhichDialogHandler.MSG_SHOW_SYNC_ERROR_DIALOG -> SyncErrorDialog.SyncErrorDialogMessageHandler.fromMessage(message) WhichDialogHandler.MSG_SHOW_DATABASE_ERROR_DIALOG -> DatabaseErrorDialog.ShowDatabaseErrorDialog.fromMessage(message) WhichDialogHandler.MSG_DO_SYNC -> IntentHandler.Companion.DoSync() @@ -134,8 +131,6 @@ abstract class DialogHandlerMessage protected constructor( val what: Int, ) { MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG(0), - MSG_SHOW_COLLECTION_IMPORT_REPLACE_DIALOG(1), - MSG_SHOW_COLLECTION_IMPORT_ADD_DIALOG(2), MSG_SHOW_SYNC_ERROR_DIALOG(3), MSG_SHOW_DATABASE_ERROR_DIALOG(6), MSG_DO_SYNC(8), diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt index 3508cbc71751..bb002aac4758 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt @@ -26,7 +26,6 @@ import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM import com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM import com.ichi2.anki.dialogs.ImportViewModel import com.ichi2.anki.utils.ext.dismissAllDialogFragments -import com.ichi2.utils.ImportUtils import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton import timber.log.Timber @@ -64,16 +63,13 @@ class ImportDialog : AsyncDialogFragment() { .setTitle(R.string.import_title) .setMessage(res().getString(R.string.import_dialog_message_add, displayFileName)) .positiveButton(R.string.import_message_add) { - // Try to handle directly for backward compatibility with activities that implement ImportDialogListener - val a = activity - if (a is ImportDialogListener) { - a.importAdd(packagePath) - a.dismissAllDialogFragments() - } else { - // If activity doesn't implement the interface, emit ViewModel event - importViewModel.triggerImportAdd(packagePath) - activity?.dismissAllDialogFragments() - } + importViewModel.registerImportRequest( + ImportViewModel.ImportRequest( + dialogType = DIALOG_IMPORT_ADD_CONFIRM, + importPath = packagePath, + ), + ) + activity?.dismissAllDialogFragments() }.negativeButton(R.string.dialog_cancel) .create() } @@ -82,16 +78,13 @@ class ImportDialog : AsyncDialogFragment() { .setTitle(R.string.import_title) .setMessage(res().getString(R.string.import_message_replace_confirm, displayFileName)) .positiveButton(R.string.dialog_positive_replace) { - // Try to handle directly for backward compatibility with activities that implement ImportDialogListener - val a = activity - if (a is ImportDialogListener) { - a.importReplace(packagePath) - a.dismissAllDialogFragments() - } else { - // If activity doesn't implement the interface, emit ViewModel event - importViewModel.triggerImportReplace(packagePath) - activity?.dismissAllDialogFragments() - } + importViewModel.registerImportRequest( + ImportViewModel.ImportRequest( + dialogType = DIALOG_IMPORT_REPLACE_CONFIRM, + importPath = packagePath, + ), + ) + activity?.dismissAllDialogFragments() }.negativeButton(R.string.dialog_cancel) .create() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt index 848b1c0b0790..f9451c5679a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2026 Georgios Michelakis * * 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 @@ -17,23 +17,27 @@ package com.ichi2.anki.dialogs import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber class ImportViewModel : ViewModel() { - private val _importAddFlow = MutableSharedFlow() - val importAddFlow = _importAddFlow.asSharedFlow() + private val pendingImportRequestState = MutableStateFlow(null) - private val _importReplaceFlow = MutableSharedFlow() - val importReplaceFlow = _importReplaceFlow.asSharedFlow() + val pendingImportRequest: StateFlow = pendingImportRequestState - fun triggerImportAdd(path: String) { - viewModelScope.launch { _importAddFlow.emit(path) } + fun registerImportRequest(request: ImportRequest) { + Timber.d("Import dialog requested: %s", request.dialogType) + pendingImportRequestState.value = request } - fun triggerImportReplace(path: String) { - viewModelScope.launch { _importReplaceFlow.emit(path) } + fun clearImportRequest() { + Timber.d("Clearing pending import dialog request") + pendingImportRequestState.value = null } + + data class ImportRequest( + val dialogType: ImportDialog.Type, + val importPath: String, + ) } diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt index e4b5d10c9f72..4ade89361974 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt @@ -22,14 +22,12 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Message import android.provider.OpenableColumns import androidx.annotation.CheckResult import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import com.ichi2.anki.AnkiActivity +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProvider import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.DeckPicker import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.common.coroutines.applicationScope @@ -37,12 +35,10 @@ import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.exception.ManuallyReportedException import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.compat.CompatHelper -import com.ichi2.anki.dialogs.DialogHandler -import com.ichi2.anki.dialogs.DialogHandlerMessage import com.ichi2.anki.dialogs.ImportDialog +import com.ichi2.anki.dialogs.ImportViewModel import com.ichi2.anki.onSelectedCsvForImport import com.ichi2.anki.servicelayer.DebugInfoService -import com.ichi2.anki.showImportDialog import kotlinx.coroutines.launch import org.jetbrains.annotations.Contract import timber.log.Timber @@ -247,7 +243,7 @@ object ImportUtils { exception = details.userFacingException, ) } - sendShowImportFileDialogMsg(tempOutDir) + sendShowImportFileDialogMsg(context, tempOutDir) return ImportResult.Success } @@ -467,21 +463,33 @@ object ImportUtils { } /** - * Send a Message to AnkiDroidApp so that the DialogMessageHandler shows the Import apkg dialog. + * Register a pending import dialog request so an active activity can show it when resumed. + * + * @param context context used to resolve the owning [ImportViewModel] * @param importPath path of to apkg file which will be imported */ - private fun sendShowImportFileDialogMsg(importPath: String) { + private fun sendShowImportFileDialogMsg( + context: Context, + importPath: String, + ) { // Get the filename from the path val filename = File(importPath).name - val dialogMessage = + val dialogType = if (isCollectionPackage(filename)) { - CollectionImportReplace(importPath) + ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM } else { - CollectionImportAdd(importPath) + ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM } - // Store the message in AnkiDroidApp message holder, which is loaded later in AnkiActivity.onResume - DialogHandler.storeMessage(dialogMessage.toMessage()) + (context as? FragmentActivity)?.let { activity -> + val importViewModel = ViewModelProvider(activity)[ImportViewModel::class.java] + importViewModel.registerImportRequest( + ImportViewModel.ImportRequest( + dialogType = dialogType, + importPath = importPath, + ), + ) + } } @SuppressLint("LocaleRootUsage") @@ -504,62 +512,6 @@ object ImportUtils { } } } - - /** Show confirmation dialog asking to confirm import with replace when file called "collection.apkg" */ - class CollectionImportReplace( - private val importPath: String, - ) : DialogHandlerMessage( - which = WhichDialogHandler.MSG_SHOW_COLLECTION_IMPORT_REPLACE_DIALOG, - analyticName = "ImportReplaceDialog", - ) { - override fun handleAsyncMessage(activity: AnkiActivity) { - // Only DeckPicker should show import confirmation dialogs. - // If another activity is resumed, keep this message queued until DeckPicker resumes. - if (activity is DeckPicker) { - activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM, importPath) - } else { - DialogHandler.storeMessage(toMessage()) - } - } - - override fun toMessage(): Message = - Message.obtain().apply { - data = bundleOf("importPath" to importPath) - what = this@CollectionImportReplace.what - } - - companion object { - fun fromMessage(message: Message): CollectionImportReplace = CollectionImportReplace(message.data.getString("importPath")!!) - } - } - - /** Show confirmation dialog asking to confirm import with add */ - class CollectionImportAdd( - private val importPath: String, - ) : DialogHandlerMessage( - WhichDialogHandler.MSG_SHOW_COLLECTION_IMPORT_ADD_DIALOG, - "ImportAddDialog", - ) { - override fun handleAsyncMessage(activity: AnkiActivity) { - // Only DeckPicker should show import confirmation dialogs. - // If another activity is resumed, keep this message queued until DeckPicker resumes. - if (activity is DeckPicker) { - activity.showImportDialog(ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM, importPath) - } else { - DialogHandler.storeMessage(toMessage()) - } - } - - override fun toMessage(): Message = - Message.obtain().apply { - data = bundleOf("importPath" to importPath) - what = this@CollectionImportAdd.what - } - - companion object { - fun fromMessage(message: Message): CollectionImportAdd = CollectionImportAdd(message.data.getString("importPath")!!) - } - } } sealed class ImportResult { From f388fd521e4cd9a5677d9e472c5be2e8da7c7773 Mon Sep 17 00:00:00 2001 From: mixelas Date: Mon, 18 May 2026 12:10:48 +0300 Subject: [PATCH 5/5] fix: parcelable ImportRequest and persist pending import fallback --- .../main/java/com/ichi2/anki/DeckPicker.kt | 21 +++++++++++++++++++ .../com/ichi2/anki/dialogs/ImportViewModel.kt | 5 ++++- .../main/java/com/ichi2/utils/ImportUtils.kt | 17 +++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 23f97a914869..d27c6b4087f9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -558,6 +558,27 @@ open class DeckPicker : importViewModel.clearImportRequest() } } + // If an external intent saved a pending import request (see ImportUtils), restore it now. + sharedPrefs().getString("pending_import_path", null)?.let { pendingPath -> + val typeCode = sharedPrefs().getInt("pending_import_dialog_type", -1) + val dialogType = + if (typeCode == com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM.code) { + com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_REPLACE_CONFIRM + } else { + com.ichi2.anki.dialogs.ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM + } + + importViewModel.registerImportRequest( + ImportViewModel.ImportRequest( + dialogType = dialogType, + importPath = pendingPath, + ), + ) + sharedPrefs().edit { + remove("pending_import_path") + remove("pending_import_dialog_type") + } + } } pullToSyncWrapper = diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt index f9451c5679a4..c381decaa261 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt @@ -16,9 +16,11 @@ package com.ichi2.anki.dialogs +import android.os.Parcelable import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import timber.log.Timber class ImportViewModel : ViewModel() { @@ -36,8 +38,9 @@ class ImportViewModel : ViewModel() { pendingImportRequestState.value = null } + @Parcelize data class ImportRequest( val dialogType: ImportDialog.Type, val importPath: String, - ) + ) : Parcelable } diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt index 4ade89361974..6b13a4f83933 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt @@ -25,6 +25,7 @@ import android.net.Uri import android.provider.OpenableColumns import androidx.annotation.CheckResult import androidx.appcompat.app.AlertDialog +import androidx.core.content.edit import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import com.ichi2.anki.AnkiDroidApp @@ -38,6 +39,7 @@ import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.dialogs.ImportDialog import com.ichi2.anki.dialogs.ImportViewModel import com.ichi2.anki.onSelectedCsvForImport +import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.servicelayer.DebugInfoService import kotlinx.coroutines.launch import org.jetbrains.annotations.Contract @@ -481,14 +483,25 @@ object ImportUtils { } else { ImportDialog.Type.DIALOG_IMPORT_ADD_CONFIRM } - (context as? FragmentActivity)?.let { activity -> - val importViewModel = ViewModelProvider(activity)[ImportViewModel::class.java] + // Register import request or persist fallback + if (context is androidx.fragment.app.FragmentActivity) { + val activity = context + val importViewModel = androidx.lifecycle.ViewModelProvider(activity)[ImportViewModel::class.java] importViewModel.registerImportRequest( ImportViewModel.ImportRequest( dialogType = dialogType, importPath = importPath, ), ) + } else { + try { + AnkiDroidApp.instance.sharedPrefs().edit { + putString("pending_import_path", importPath) + putInt("pending_import_dialog_type", dialogType.code) + } + } catch (e: Exception) { + Timber.w(e, "Failed to persist pending import request") + } } }