Chore: isowntask fixes#3011
Merged
Merged
Conversation
The 'Up arrow tracking logs' commit added logcat calls in BackstackController that run during BackstackControllerTest and DeepLinkNavigationTest, where the default NoLog logger throws. Install TestLogcatLoggingRule in both so the suite stays green.
Two defensive fixes for the report of a 'deep-link stack' (Home shown with a back arrow instead of the nav bar) appearing after a plain icon launch + login, with no deep-link action by the user. 1. Snapshot-back BackstackController.isOwnTask. The lone-deep-link chrome derives the back-arrow-vs-nav-bar decision partly from isTaskRoot, previously read via a non-observable () -> Boolean lambda. A normal lone Home then renders as a deep-link stack whenever isTaskRoot is false, and could latch that stale reading. isOwnTask is now a mutableStateOf-backed Boolean, refreshed from isTaskRoot in onCreate and on every onResume, so the chrome recomposes and self-corrects. 2. Time-bound the persisted pendingDeepLink. It survives process death (for the mid-OTP case) but was never age-bounded, so a link stashed long ago could bleed into an unrelated later login as a lone stack. It now carries a stash timestamp; on restore a link older than 30 minutes (or with no timestamp, or a backwards clock) is dropped. Adds regression tests for the isOwnTask toggle, the pending-timestamp lifecycle, and the 30-minute freshness boundary.
Follow-up cleanups on the two defensive fixes: - Seed BackstackController.isOwnTask from the Activity's isTaskRoot at first construction (threaded through NavRetainedViewModel) instead of relying on the true default and the onCreate wiring running before any read. onResume remains the authoritative refresh; the default stays only for unit tests. - Collapse the isOwnTask backing field + explicit accessors into a single 'by mutableStateOf' delegate. - In NavigationStateBridge.restoreAndPersist, sample the clock once (was twice, letting the logged age disagree with the freshness decision) and use takeIf. - Express the pending-deep-link window as 30.minutes (kotlin.time.Duration), the repo idiom, instead of raw millis arithmetic.
Verbose logs are skipped in datadog and firebase, only shown locally
The lone-deep-link chrome was driven by isTaskRoot, which is false both for a genuinely foreign-hosted deep link (where we want the Up/escape affordance) and for a non-root second MainActivity stacked in our own task (a launcher relaunch or a notification that fronted our existing task), where we do not. The latter rendered a back arrow and no nav bar on a normal Home. Discriminate on the launch flags instead: FLAG_ACTIVITY_NEW_TASK means the system placed us in our own task (new or brought-to-front), so we are own-task even when not its root; only a launch without NEW_TASK joins the caller's task, which with not-task-root is the one genuinely foreign-hosted case. MainActivity now feeds isOwnTaskForLaunch(isTaskRoot, intent.flags) into the controller in onCreate and onResume. Verified across the full launch matrix (Android Studio, launcher cold/relaunch, notification cold/warm, foreign deep link, escape-to-own-task, config change, recents and process-death restore). Keeps a single per-launch breadcrumb log in onCreate (DeepLinkStackDebug) for future task/back-stack diagnosis; removes the rest of the investigation logging. Adds IsOwnTaskForLaunchTest for the discriminator.
Rename isOwnTaskForLaunch -> isHostedInForeignTask and flip it to name the one exceptional case directly: !isTaskRoot && no NEW_TASK flag (i.e. we joined the caller's task). Both clauses now read as the facts that define foreignness, which is clearer than a positive catch-all built from an OR. Call site reads isOwnTask = !isHostedInForeignTask(...). Behaviour is identical. Renames the test and the onCreate breadcrumb field to match.
panasetskaya
approved these changes
Jun 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stop relying purely on isTaskRoot to determine if we are in a deep link to show the up arrow. This also happens when launching from Android Studio or from clicking a notification and then going home and clicking on the launched icon.
More discussion here: https://hedviginsurance.slack.com/archives/C03HT2JRDPG/p1782462903853589
AI-generated PR description
Summary
Members occasionally saw a "deep-link stack" on Home (a back arrow and no bottom nav bar) after launching from the icon and logging in, without doing anything deep-link related. This fixes that, plus a couple of related navigation-robustness issues found along the way.
Root cause
BackstackController.loneDeepLinkChrome chose between the nav bar and the lone-deep-link Up bar partly from isTaskRoot. But isTaskRoot is false in two different situations:
Case 2 rendered the back arrow on a normal Home. It only reproduces when the task was first established by a non-launcher entry (notification, deep link, Android Studio) and the user then taps the icon, which is why it was intermittent.
Fix
Decide foreignness from the launch flags, not isTaskRoot:
isHostedInForeignTask(isTaskRoot, launchFlags) =
!isTaskRoot && (launchFlags and FLAG_ACTIVITY_NEW_TASK) == 0
We're foreign-hosted only when we're not the task root and were launched without NEW_TASK (Android's default of joining the caller's task). NEW_TASK being set means the system gave us our own task (fresh or brought-to-front), so launcher relaunches and notification taps (both carry NEW_TASK) are correctly own-task even when not root. The chrome uses isOwnTask = !isHostedInForeignTask(...), refreshed in onCreate and onResume.
Also included
Testing
Unit tests: IsHostedInForeignTaskTest, plus BackstackControllerTest / Navigs (snapshot-backed isOwnTask, pending-timestamp lifecycle, 30-min boundary).Manually verified on-device across the full launch matrix:
┌─────────────────────────────────────────────────┬───────────────────┬──────────────┬──────────┬─────────────────────────┐
│ Launch │ NEW_TASK? │ is result │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ Launcher cold / relaunch (dup) │ yes │ trur ✓ │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ Notification cold / warm │ yes │ trur / lone screen ✓ │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ Foreign deep link (another app) │ no │ fal → escape ✓ │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ Android Studio │ yes │ trur ✓ │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ escape-to-own-task relaunch │ yes (+CLEAR_TASK) │ trur ✓ │
├─────────────────────────────────────────────────┼───────────────────┼──────────────┼──────────┼─────────────────────────┤
│ Config change / Recents / process-death restore │ yes │ trur ✓ │
└─────────────────────────────────────────────────┴───────────────────┴──────────────┴──────────┴─────────────────────────┘
Known limitation (out of scope)
The duplicate MainActivity itself still exists when the task was established by a non-launcher entry (two back stacks; Back from the duplicate's Home reveals the older instance). Pre-existing (launchMode="standard"); the chrome now renders correctly regae would mean launchMode="singleTask"/singleTop, a larger change to deep-linkrouting, the escapeToOwnTask mechanism, and the multiple-back-stack model — raising separately as an "is this WAI?" question first.