Skip to content

fix: handle import dialog events via ViewModel lifecycle#20992

Open
mixelas wants to merge 4 commits into
ankidroid:mainfrom
mixelas:fix/issue-5075-dialoghandler-cast
Open

fix: handle import dialog events via ViewModel lifecycle#20992
mixelas wants to merge 4 commits into
ankidroid:mainfrom
mixelas:fix/issue-5075-dialoghandler-cast

Conversation

@mixelas
Copy link
Copy Markdown

@mixelas mixelas commented May 8, 2026

Purpose / Description

Fix a crash where import dialog handling could run after activity changes and assume a DeckPicker-like context.
The issue is an async UI event path that may outlive the originating screen, causing invalid activity assumptions and ClassCastException in rare lifecycle transitions.

Fixes

Approach

Introduced an import event pipeline through a dedicated ImportViewModel.
ImportDialog now emits import actions as ViewModel events instead of relying only on direct activity casting.
AnkiActivity observes those events with lifecycle awareness, so dialog-triggered import actions are consumed only when the UI is in a safe state.
Kept backward-compatible behavior: if the current activity already implements ImportDialogListener, handling still works directly.
Net effect: removes brittle activity assumptions and aligns async dialog consumption with lifecycle-safe delivery.

How Has This Been Tested?

Build and run AnkiDroid in debug mode.
Start app flow that can trigger import confirmation dialogs.
Trigger lifecycle transitions around dialog timing

Test configuration used:
Android debug build
Emulator/device setup used for local development
Pre-commit hooks passed during commit, including ktlint

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@mixelas mixelas marked this pull request as ready for review May 9, 2026 12:06
@sanjaysargam
Copy link
Copy Markdown
Member

@mixelas Are you able to reproduce this issue ? If yes, May I know the exact reproduction steps, coz I can't reproduce

@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 10, 2026

@sanjaysargam I wasn’t able to reproduce it reliably either. The issue appears to be timing/lifecycle dependent, so there isn’t a deterministic set of steps that reproduces it every time. The closest scenario I could infer from the crash reports and code path is:
Open AnkiDroid through a non-DeckPicker path, such as Reviewer first or via a file manager.
Trigger an import flow while the current activity is transitioning or no longer the one that originally started the dialog.
The import confirmation dialog is then handled by an activity that does not implement the expected listener, which leads to the crash.
The fix is meant to make that path safe even when the exact reproduction is hard to hit.

@david-allison
Copy link
Copy Markdown
Member

I'd propose closing this until reproduction steps and a regression test exists,
it should be a last-ditch effort to introduce a fix for a bugs which we can't reproduce.

@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 11, 2026

@lukstbit assigned this to me and explicitly approved this ViewModel approach as the right solution. I followed his guidance and implemented it. This issue has real ACRA crash reports in production and the fix uses a standard pattern already in AnkiDroid. I can add unit/integration tests to strengthen confidence. The timing-dependent nature makes 100% repro hard, but the fix is architecturally sound.

@lukstbit lukstbit self-requested a review May 11, 2026 09:26
@david-allison
Copy link
Copy Markdown
Member

david-allison commented May 11, 2026

I'll wait for a reply, but it's unexpected for someone to produce a fix before reproducing an issue.

Our contributor guide rules aren't "hard" rules, but I feel skipping reproduction steps often requires a strong justification. I've had cases where I was able to remove hundreds of lines of code once a problem was properly understood

Copy link
Copy Markdown
Member

@lukstbit lukstbit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a step in the right direction but:

  • I expect the previous related message code to be removed
  • I expect that the import dialog isn't shown for activities that can't handle it(only DeckPicker should handle it)
  • I expect that if we end up in another activity by the time the dialog should be shown to have DeckPicker show it when coming back

Note: I've made a PR for one of the other async dialogs having this issue in #21018

Kind of reproduce this by adding the patch below in DeckPicker:

diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
index 823542920d..d0906c4185 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
@@ -197,6 +197,7 @@ import com.ichi2.utils.show
 import com.ichi2.utils.title
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
@@ -357,6 +358,7 @@ open class DeckPicker :
                     lifecycleScope.launch {
                         withProgress(message = getString(R.string.import_preparing_file)) {
                             withContext(Dispatchers.IO) {
+                                delay(4000)
                                 onSelectedPackageToImport(it.data!!)
                             }
                         }

Start the import process of an apkg -> select a file -> come back and seeing the progress -> while the progress is running go to home -> wait a bit more until the delay passes -> start CardBrowser through a shortcut -> see the ImportAdd dialog -> confirm and crash

Comment thread AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt Outdated
@lukstbit
Copy link
Copy Markdown
Member

lukstbit commented May 11, 2026

I'd propose closing this until reproduction steps and a regression test exists,
it should be a last-ditch effort to introduce a fix for a bugs which we can't reproduce.

While we can't reproduce this exactly it's clear IMO what's happening.
Sending messages to the main Looper to show dialogs was primitive 5 years ago and even worse now. Any improvements(and I do consider these improvements) is a step forward.

Plus this back and forth with destinations through a ViewModel is not something new and we use it.

@lukstbit lukstbit added the Needs Author Reply Waiting for a reply from the original author label May 11, 2026
@mixelas mixelas force-pushed the fix/issue-5075-dialoghandler-cast branch 2 times, most recently from 1a7c390 to 29ad77b Compare May 11, 2026 14:36
@mixelas mixelas closed this May 11, 2026
@mixelas mixelas force-pushed the fix/issue-5075-dialoghandler-cast branch from beab2fe to 7dc302e Compare May 11, 2026 15:10
@mixelas mixelas reopened this May 11, 2026
@david-allison
Copy link
Copy Markdown
Member

Given

Let's go back to the ViewModel approach. Sorry for the mixed messages

@mixelas mixelas force-pushed the fix/issue-5075-dialoghandler-cast branch from 9e668d7 to 8ce046f Compare May 12, 2026 10:13
@mixelas mixelas force-pushed the fix/issue-5075-dialoghandler-cast branch from 8ce046f to d24f09a Compare May 12, 2026 10:15
Copy link
Copy Markdown
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with the approach in #21018 and replace a subclass of DialogHandlerMessage with a full ViewModel-level implementation.

  • #21018 is a much better pattern for handling messages than what we have, and this will be a great improvement

Comment thread AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt Outdated
@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 12, 2026

I switched it to the ViewModel-based approach from #21018, so the import dialog is now handled the same way as that async dialog fix.

Copy link
Copy Markdown
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheers!

Comment thread AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Comment thread AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt
Comment thread AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportViewModel.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ImportDialog.kt Outdated
@mixelas mixelas force-pushed the fix/issue-5075-dialoghandler-cast branch from 3ef66f1 to cb9a264 Compare May 12, 2026 17:15
@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 12, 2026

Thanks a lot for the detailed review and patience here. I really appreciate it. I’m very open to further changes. I’m trying to learn the preferred project patterns as I go.

pendingImportRequestState.value = null
}

data class ImportRequest(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this moves to SavedStateHandle (per #21018 pattern), this will need @parcelize to survive process death

}
// Store the message in AnkiDroidApp message holder, which is loaded later in AnkiActivity.onResume
DialogHandler.storeMessage(dialogMessage.toMessage())
(context as? FragmentActivity)?.let { activity ->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntentHandler extends Activity, not FragmentActivity, so context as? FragmentActivity returns null here, preventing the import dialog from showing for externally opened .apkg files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Author Reply Waiting for a reply from the original author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants