Skip to content

Fix NavigationController crash from onNewIntent before Compose attaches#284

Merged
d4rken merged 1 commit into
mainfrom
fix-navcontroller-onnewintent-race
May 4, 2026
Merged

Fix NavigationController crash from onNewIntent before Compose attaches#284
d4rken merged 1 commit into
mainfrom
fix-navcontroller-onnewintent-race

Conversation

@d4rken

@d4rken d4rken commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a Google Play crash report on Samsung Galaxy S24 Ultra running Android 16 Beta (SDK 36):

Caused by java.lang.IllegalStateException:
  at eu.darken.octi.common.navigation.NavigationController.getBackStack (NavigationController.kt:15)
  at eu.darken.octi.common.navigation.NavigationController.popTo (NavigationController.kt:61)
  at eu.darken.octi.main.ui.MainActivity.onNewIntent (MainActivity.kt:123)
  at android.app.ActivityThread.deliverNewIntents
  at android.app.ActivityThread.performResumeActivity

Root cause

NavigationController._backStack is registered via setup(backStack) from inside the setContent { } Compose lambda — asynchronous, frame-scheduled. MainActivity.onNewIntent runs synchronously and the stack trace shows it firing inside performResumeActivity → deliverNewIntents. There is a window during activity recreation (config change, dark-mode flip) or singleTask warm intent delivery where onNewIntent runs before the Compose lambda has executed. _backStack is still null, and popTo(Nav.Main.Dashboard) throws.

Fix

Move the popTo(Dashboard) step out of onNewIntent and into a Compose-level LaunchedEffect. onNewIntent becomes a pure intent-parsing entry point that emits VM events; the navigation work happens once Compose has attached, where the back stack is guaranteed initialized.

  • Add MainActivityVM.deeplinkAccepted: SingleEventFlow<Unit> — fired after a successful widget or file-share-notification deeplink parse (not for system-share intents)
  • MainActivity.onNewIntent now just calls vm.handleDeeplinkIntent(intent) and returns
  • New LaunchedEffect inside setContent { } collects deeplinkAcceptednavCtrl.popTo(Nav.Main.Dashboard)
  • incomingShareEvents collector switched to goTo(FileShareList, popUpTo = Dashboard) (single atomic call) so it does not race with the parallel popTo collector
  • NavigationController is unchanged — strict error() getter stays, contract not weakened

This matches the canonical pattern already used in permission-pilot (intent → VM event → Compose-level LaunchedEffectnavCtrl.*).

Test plan

  • ./gradlew :app:testFossDebugUnitTest passes (6 new VM tests covering deeplinkAccepted across widget / file-share / system-share / unrecognized / not-onboarded cases)
  • ./gradlew assembleFossDebug compiles cleanly
  • Manual: open Octi to Dashboard → navigate to Settings → share a file from another app via system share sheet → confirm back stack lands at [Dashboard, FileShareList]
  • Manual: open Octi to Dashboard → navigate to Settings → tap a widget on the home screen → confirm back stack lands at [Dashboard] and the module detail sheet opens
  • Manual: with onboarding incomplete, repeat both — confirm we stay on Welcome and no nav happens

Move popTo(Dashboard) out of MainActivity.onNewIntent and into a Compose-level LaunchedEffect that collects a new MainActivityVM.deeplinkAccepted SingleEventFlow. Use goTo with popUpTo=Dashboard for incoming-share so it does not race with the parallel popTo collector.

onNewIntent could fire before the setContent{} lambda registered the back stack with NavigationController (timing-sensitive race observed on Samsung Galaxy S24 Ultra running Android 16 Beta), causing IllegalStateException at NavigationController.kt:15 from popTo. Same canonical pattern as permission-pilot.
@d4rken d4rken added the bug Something isn't working label May 4, 2026
d4rken added a commit to d4rken-org/capod that referenced this pull request May 4, 2026
Move consumeUpgradeExtra() out of MainActivity.onNewIntent and into a Compose-level LaunchedEffect that collects a new MainActivity.warmIntents SingleEventFlow.

onNewIntent could fire before the setContent{} lambda registered the back stack with NavigationController, leaving navCtrl.goTo(Nav.Main.Upgrade) to throw IllegalStateException("NavigationController not initialized"). Same race shipped a fix for in octi (d4rken-org/octi#284).
@d4rken d4rken merged commit ee60e53 into main May 4, 2026
15 checks passed
@d4rken d4rken deleted the fix-navcontroller-onnewintent-race branch May 4, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant