Skip to content

Android: close screens left open when the app is reopened from the launcher - #686

Open
kurexw wants to merge 1 commit into
OpenStrap:mainfrom
kurexw:fix/android-relaunch-closes-screens
Open

kurexw wants to merge 1 commit into
OpenStrap:mainfrom
kurexw:fix/android-relaunch-closes-screens

Conversation

@kurexw

@kurexw kurexw commented Nov 10, 2026 •

Copy link
Copy Markdown

Fixes the Android half of #650.

Why

On Android, closing the app (swiping it from recents) and opening it again from the launcher lands back on whatever screen was on top, e.g. inside Settings. The cause is deliberate plumbing: MainActivity attaches to the engine EdgeApplication caches and never destroys (shouldDestroyEngineWithHost() = false), so background Bluetooth keeps running. The Activity goes, the Dart side and its Navigator stay, and the next Activity attaches to the same stack.

On iOS the process ends, so a relaunch starts fresh on the saved tab with nothing pushed on top. This makes Android do the same.

Scope note: the issue asks to always open on Home. Reopening on the last tab is intentional on both platforms (restoredShellTab() / Prefs.shellTabV2, "so an upgrade lands where the user was"), so this PR keeps it and only closes the screens pushed above the tab. If you'd rather always land on Today, that's a one-line follow-up in _ShellState.

What changes

  • MainActivity.onCreate: notes whether the cached engine already existed before ensureEngine. When it did, there is no savedInstanceState, and the intent is the launcher's (ACTION_MAIN + CATEGORY_LAUNCHER), it invokes relaunched on a new openstrap/activity channel.
    • Not on a system restore (savedInstanceState != null): Android recreated the Activity and the user expects the screen back.
    • Not for notification taps (SELECT_NOTIFICATION, flutter_local_notifications 18.0.1), home_widget taps (HOME_WIDGET_LAUNCH_ACTION), or our own StrapWidgets tap (no action): those keep routing as before.
    • Rotation / dark mode never reach onCreate (configChanges in the manifest).
  • lib/platform/activity_relaunch.dart (new): handles relaunched by popUntil(isFirst) on the root navigator. stop() only clears its own owner's handler, so a replaced owner's dispose (which runs after its successor's initState) can't silence the successor.
  • lib/app.dart (_ShellState): listens in initState, stops in dispose. The tab is untouched. A running workout stays reachable through the existing _LiveSessionBar above every tab.

Tests

  • New test/activity_relaunch_test.dart (4 widget tests, driven through the real method channel): relaunched closes every pushed screen back to the tab; any other method does nothing; a disposed owner is a no-op; a replaced owner's stop doesn't remove its successor's handler.
  • flutter analyze: no issues.
  • flutter test --concurrency=1: 7047 passed, 28 skipped, 1 failed. The failure is health_workout_export_delete_gate_test, which fails on any macOS host on current main and is fixed by Pin the Health Connect delete-gate test to Health Connect (fails on macOS) #685 (it pins that test to Health Connect). Two files (synthetic_oura_day_test, whoop_fallback_night_banked_test) reported "did not complete" in that run while other tests were running on the same machine; re-run on their own, all 10 of their tests pass.

Verified on an Android emulator (API 37, arm64, debug build; no band paired, onboarding skipped). The Activity was ended with am stack remove <task>, which destroys it while the process (and the cached engine) stays alive, the same state as swiping the app away while the foreground service keeps the process. The PID was checked before and after each step.

Step main @ 69341a6 this branch
Sleep tab → Profile → Settings, task removed, relaunched with the launcher intent (MAIN/LAUNCHER, LaunchState: WARM, same PID) Settings is still open (#650) Sleep tab, nothing pushed on top
Settings open, rotate to landscape and back, toggle dark mode unchanged unchanged (no reset)
Settings open, task removed, relaunched with act=SELECT_NOTIFICATION (the action flutter_local_notifications uses; no payload) — Settings stays: the reset is not sent for a non-launcher intent

The Kotlin compiles and runs. Not covered: a real notification tap end to end (no band, so the app posted none; the third row exercises the intent gate it depends on), and a physical device.

🤖 Generated with Claude Code

Summary by Sourcery

Restore Android launcher relaunches to the saved tab without retaining screens pushed above it.

Bug Fixes:

  • Reset the Android navigation stack to the saved tab when the app is reopened from the launcher after its retained engine outlives the Activity.
  • Preserve existing navigation for system restores, notification taps, and widget launches.

Enhancements:

  • Add activity relaunch handling that safely manages navigator ownership across shell lifecycle changes while leaving the selected tab and active workout accessible.

Tests:

  • Add widget tests covering relaunch navigation resets, ignored method calls, disposed owners, and replacement-owner lifecycle behavior.

Summary by CodeRabbit

  • Bug Fixes
    • Relaunching the app now returns navigation to its first screen.

On Android the Flutter engine outlives the Activity (it is cached and
never destroyed, so background Bluetooth keeps running). Swiping the
app away and opening it again from the launcher attached a new
Activity to the same Navigator, so the app reopened inside whatever
screen was on top, e.g. Settings (OpenStrap#650). On iOS the process ends and a
relaunch starts on the saved tab with nothing above it.

MainActivity now notes whether the engine already existed before
ensureEngine, and when it did, with no savedInstanceState and the
launcher's ACTION_MAIN/CATEGORY_LAUNCHER intent, sends `relaunched` on
a new openstrap/activity channel. The shell handles it by popping to
the first route. The saved tab is kept: restoring it is intentional
(restoredShellTab). System restores, notification taps and widget taps
are untouched; rotation and dark mode never reach onCreate
(configChanges).
@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

API

Add the openstrap/activity method-channel contract and Dart handler that pops the root navigator to its first route on relaunched.

  • android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/MainActivity.kt L7-8, L32-34, L62-64
  • lib/platform/activity_relaunch.dart L1-37

Uses of the API

Connect the shell lifecycle to the relaunch handler so it can access the navigator while mounted and relinquishes its handler on disposal.

  • lib/app.dart L13-614 (4 small edits)

Implementation

Detect a launcher relaunch that attaches to an existing engine, while excluding system restores and non-launcher intents, before notifying Dart.

  • android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/MainActivity.kt L41-61

Tests

Exercise the method-channel behavior, including stack popping, ignored calls, missing navigators, and replacement-owner cleanup.

  • test/activity_relaunch_test.dart L1-86

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The app now handles Android activity relaunch method calls by returning the shell navigator to its first route. The shell registers and unregisters the navigator callback with its lifecycle.

Changes: Activity relaunch handling

Layer / File(s) Summary
Handle relaunch calls
lib/platform/activity_relaunch.dart
ActivityRelaunch handles relaunched calls by popping the current navigator to its first route. It removes the method handler only when the stopping callback matches the registered callback.
Connect the shell navigator
lib/app.dart
_ShellState registers a callback that returns its navigator when mounted and unregisters it during disposal.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant AndroidActivity
  participant ActivityRelaunch
  participant ShellState
  participant NavigatorState
  AndroidActivity->>ActivityRelaunch: Send relaunched method call
  ActivityRelaunch->>ShellState: Resolve registered navigator callback
  ShellState-->>ActivityRelaunch: Return navigator when mounted
  ActivityRelaunch->>NavigatorState: Pop routes until first route
Loading

Possibly related issues

  • OpenStrap/edge#650: The issue reports Android reopening the last visited screen. This change handles relaunches by popping the current navigator to its first route.

Suggested reviewers: abdulsaheel

Merge Risk: Minimal · up to 14fca

No actionable merge-blocking issue was established. Android device behavior remains unverified.

Pre-merge checks: Passed 4

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: closing screens left open when the app is reopened from the launcher.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: OpenStrap/edge
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 69341a6
  • Layers: Repository YAML
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: fb2c79bd-34ea-4db0-b383-e1e84a589138
Commits

Reviewing files that changed from the base of the PR and between 69341a6 and 14fca0b.

Files ignored due to path filters: 2

  • android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/MainActivity.kt is excluded by !android/**
  • test/activity_relaunch_test.dart is excluded by !test/**
Files selected for processing: 2

  • lib/app.dart
  • lib/platform/activity_relaunch.dart

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/platform/activity_relaunch.dart" line_range="24" />
<code_context>
+    _navigator = navigator;
+    channel.setMethodCallHandler((call) async {
+      if (call.method != 'relaunched') return;
+      _navigator?.call()?.popUntil((route) => route.isFirst);
+    });
+  }
</code_context>
<issue_to_address>
**Launcher relaunch freezes during active capture**

When the launcher relaunch arrives while CalmBreathing is running or ECG capture is busy, `popUntil` repeatedly tries to pop the guarded route, but its `PopScope` rejects the pop; the loop cannot advance while the route remains topmost, so the UI isolate hangs and the app freezes.

Close guarded screens asynchronously before popping the remaining routes, or otherwise make relaunch stack clearing advance without retrying a pop that `PopScope` rejects.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: lib/platform/activity_relaunch.dart:24


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

_navigator = navigator;
channel.setMethodCallHandler((call) async {
if (call.method != 'relaunched') return;
_navigator?.call()?.popUntil((route) => route.isFirst);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Launcher relaunch freezes during active capture

When the launcher relaunch arrives while CalmBreathing is running or ECG capture is busy, popUntil repeatedly tries to pop the guarded route, but its PopScope rejects the pop; the loop cannot advance while the route remains topmost, so the UI isolate hangs and the app freezes.

Close guarded screens asynchronously before popping the remaining routes, or otherwise make relaunch stack clearing advance without retrying a pop that PopScope rejects.

Prompt for AI agents
In `lib/platform/activity_relaunch.dart` at line 24:

**Launcher relaunch freezes during active capture**

When the launcher relaunch arrives while CalmBreathing is running or ECG capture is busy, `popUntil` repeatedly tries to pop the guarded route, but its `PopScope` rejects the pop; the loop cannot advance while the route remains topmost, so the UI isolate hangs and the app freezes.

Close guarded screens asynchronously before popping the remaining routes, or otherwise make relaunch stack clearing advance without retrying a pop that `PopScope` rejects.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant