Repository navigation
Conversation
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).
Reviewer's GuideAPIAdd the
Uses of the APIConnect the shell lifecycle to the relaunch handler so it can access the navigator while mounted and relinquishes its handler on disposal.
ImplementationDetect a launcher relaunch that attaches to an existing engine, while excluding system restores and non-launcher intents, before notifying Dart.
TestsExercise the method-channel behavior, including stack popping, ignored calls, missing navigators, and replacement-owner cleanup.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
There was a problem hiding this comment.
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
| _navigator = navigator; | ||
| channel.setMethodCallHandler((call) async { | ||
| if (call.method != 'relaunched') return; | ||
| _navigator?.call()?.popUntil((route) => route.isFirst); |
There was a problem hiding this comment.
🟠 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.
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:
MainActivityattaches to the engineEdgeApplicationcaches 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 beforeensureEngine. When it did, there is nosavedInstanceState, and the intent is the launcher's (ACTION_MAIN+CATEGORY_LAUNCHER), it invokesrelaunchedon a newopenstrap/activitychannel.savedInstanceState != null): Android recreated the Activity and the user expects the screen back.SELECT_NOTIFICATION, flutter_local_notifications 18.0.1), home_widget taps (HOME_WIDGET_LAUNCH_ACTION), or our ownStrapWidgetstap (no action): those keep routing as before.onCreate(configChangesin the manifest).lib/platform/activity_relaunch.dart(new): handlesrelaunchedbypopUntil(isFirst)on the root navigator.stop()only clears its own owner's handler, so a replaced owner'sdispose(which runs after its successor'sinitState) can't silence the successor.lib/app.dart(_ShellState): listens ininitState, stops indispose. The tab is untouched. A running workout stays reachable through the existing_LiveSessionBarabove every tab.Tests
test/activity_relaunch_test.dart(4 widget tests, driven through the real method channel):relaunchedcloses every pushed screen back to the tab; any other method does nothing; a disposed owner is a no-op; a replaced owner'sstopdoesn't remove its successor's handler.flutter analyze: no issues.flutter test --concurrency=1: 7047 passed, 28 skipped, 1 failed. The failure ishealth_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.main@ 69341a6MAIN/LAUNCHER,LaunchState: WARM, same PID)act=SELECT_NOTIFICATION(the action flutter_local_notifications uses; no payload)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:
Enhancements:
Tests:
Summary by CodeRabbit