Repository navigation
Android: close screens left open when the app is reopened from the launcher #686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kurexw
wants to merge
1
commit into
OpenStrap:main
Choose a base branch
from
kurexw:fix/android-relaunch-closes-screens
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import 'package:flutter/services.dart'; | ||
| import 'package:flutter/widgets.dart'; | ||
|
|
||
| /// Android: the launcher opened the app again after it was closed (issue #650). | ||
| /// | ||
| /// The Activity goes away when the app is swiped from recents, but the Flutter | ||
| /// engine is kept alive for background Bluetooth, so the Navigator still holds | ||
| /// whatever screen was on top: reopening landed back inside Settings. On iOS the | ||
| /// process ends and a relaunch starts on the saved tab with nothing above it. | ||
| /// `MainActivity.onCreate` sends `relaunched` only for that case, and this | ||
| /// closes the pushed screens to match. The tab itself is left alone: it is | ||
| /// restored on purpose (`restoredShellTab`). | ||
| class ActivityRelaunch { | ||
| static const channel = MethodChannel('openstrap/activity'); | ||
|
|
||
| static NavigatorState? Function()? _navigator; | ||
|
|
||
| /// Handle `relaunched` while [navigator] can answer; it returns null once its | ||
| /// owner is gone. Pass the same function to [stop] when the owner is disposed. | ||
| static void listen(NavigatorState? Function() navigator) { | ||
| _navigator = navigator; | ||
| channel.setMethodCallHandler((call) async { | ||
| if (call.method != 'relaunched') return; | ||
| _navigator?.call()?.popUntil((route) => route.isFirst); | ||
| }); | ||
| } | ||
|
|
||
| /// Stops listening, unless a newer owner has called [listen] since: a | ||
| /// replaced owner's dispose runs after its successor's initState. | ||
| static void stop(NavigatorState? Function() navigator) { | ||
| // `!=`, not `identical`: two tear-offs of one instance's method are equal | ||
| // but not guaranteed identical. | ||
| if (_navigator != navigator) return; | ||
| _navigator = null; | ||
| channel.setMethodCallHandler(null); | ||
| } | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // #650: on Android the Flutter engine outlives the Activity, so a screen left | ||
| // open (Settings, a detail page) was still on top when the app was opened | ||
| // again from the launcher. `MainActivity` sends `relaunched` on | ||
| // `openstrap/activity` for exactly that case; the Dart side closes the pushed | ||
| // screens and leaves the tab underneath alone, as an iOS cold start would. | ||
|
|
||
| import 'package:flutter/material.dart'; | ||
| import 'package:flutter/services.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
|
|
||
| import 'package:openstrap_edge/platform/activity_relaunch.dart'; | ||
|
|
||
| void main() { | ||
| final navKey = GlobalKey<NavigatorState>(); | ||
| NavigatorState? nav() => navKey.currentState; | ||
|
|
||
| Future<void> fromNative(WidgetTester tester, String method) async { | ||
| await tester.binding.defaultBinaryMessenger.handlePlatformMessage( | ||
| ActivityRelaunch.channel.name, | ||
| const StandardMethodCodec().encodeMethodCall(MethodCall(method)), | ||
| (_) {}, | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
| } | ||
|
|
||
| Future<void> openTwoScreens(WidgetTester tester) async { | ||
| await tester.pumpWidget(MaterialApp( | ||
| navigatorKey: navKey, | ||
| home: const Text('tab'), | ||
| )); | ||
| for (final name in ['settings', 'detail']) { | ||
| navKey.currentState! | ||
| .push(MaterialPageRoute<void>(builder: (_) => Text(name))); | ||
| await tester.pumpAndSettle(); | ||
| } | ||
| expect(find.text('detail'), findsOneWidget); | ||
| } | ||
|
|
||
| tearDown(() => ActivityRelaunch.stop(nav)); | ||
|
|
||
| testWidgets('relaunched closes every pushed screen, back to the tab', | ||
| (tester) async { | ||
| await openTwoScreens(tester); | ||
| ActivityRelaunch.listen(nav); | ||
|
|
||
| await fromNative(tester, 'relaunched'); | ||
|
|
||
| expect(find.text('tab'), findsOneWidget); | ||
| expect(find.text('settings'), findsNothing); | ||
| expect(find.text('detail'), findsNothing); | ||
| }); | ||
|
|
||
| testWidgets('any other call leaves the screens alone', (tester) async { | ||
| await openTwoScreens(tester); | ||
| ActivityRelaunch.listen(nav); | ||
|
|
||
| await fromNative(tester, 'something_else'); | ||
|
|
||
| expect(find.text('detail'), findsOneWidget); | ||
| }); | ||
|
|
||
| testWidgets('a disposed owner (null navigator) is a no-op, not a crash', | ||
| (tester) async { | ||
| await openTwoScreens(tester); | ||
| NavigatorState? gone() => null; | ||
| ActivityRelaunch.listen(gone); | ||
|
|
||
| await fromNative(tester, 'relaunched'); | ||
|
|
||
| expect(find.text('detail'), findsOneWidget); | ||
| ActivityRelaunch.stop(gone); | ||
| }); | ||
|
|
||
| testWidgets("a replaced owner's stop does not silence its successor", | ||
| (tester) async { | ||
| await openTwoScreens(tester); | ||
| NavigatorState? old() => null; | ||
| ActivityRelaunch.listen(old); | ||
| ActivityRelaunch.listen(nav); // successor's initState | ||
| ActivityRelaunch.stop(old); // predecessor's later dispose | ||
|
|
||
| await fromNative(tester, 'relaunched'); | ||
|
|
||
| expect(find.text('tab'), findsOneWidget); | ||
| }); | ||
| } |
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.
There was a problem hiding this comment.
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,
popUntilrepeatedly tries to pop the guarded route, but itsPopScoperejects 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
PopScoperejects.Prompt for AI agents