diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 376142b9c7e55..e26d13e80f74a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -1056,6 +1056,14 @@ public void onAttach(@NonNull Context context) { delegate.onAttach(context); if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) { requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + // When Android handles a back gesture, it pops an Activity or goes back + // to the home screen. When Flutter handles a back gesture, it pops a + // route inside of the Flutter part of the app. By default, Android + // handles back gestures, so this callback is disabled. If, for example, + // the Flutter app has routes for which it wants to handle the back + // gesture, then it will enable this callback using + // setFrameworkHandlesBack. + onBackPressedCallback.setEnabled(false); } context.registerComponentCallbacks(this); } @@ -1663,9 +1671,14 @@ public boolean popSystemNavigator() { // Unless we disable the callback, the dispatcher call will trigger it. This will then // trigger the fragment's onBackPressed() implementation, which will call through to the // dart side and likely call back through to this method, creating an infinite call loop. - onBackPressedCallback.setEnabled(false); + boolean enabledAtStart = onBackPressedCallback.isEnabled(); + if (enabledAtStart) { + onBackPressedCallback.setEnabled(false); + } activity.getOnBackPressedDispatcher().onBackPressed(); - onBackPressedCallback.setEnabled(true); + if (enabledAtStart) { + onBackPressedCallback.setEnabled(true); + } return true; } } @@ -1673,6 +1686,14 @@ public boolean popSystemNavigator() { return false; } + @Override + public void setFrameworkHandlesBack(boolean frameworkHandlesBack) { + if (!getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) { + return; + } + onBackPressedCallback.setEnabled(frameworkHandlesBack); + } + @VisibleForTesting @NonNull boolean shouldDelayFirstAndroidViewDraw() { diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 65fb752f20816..fda8a7c21da97 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -518,6 +518,7 @@ protected FlutterFragment createFlutterFragment() { ? TransparencyMode.opaque : TransparencyMode.transparent; final boolean shouldDelayFirstAndroidViewDraw = renderMode == RenderMode.surface; + final boolean shouldAutomaticallyHandleOnBackPressed = true; if (getCachedEngineId() != null) { Log.v( @@ -542,6 +543,7 @@ protected FlutterFragment createFlutterFragment() { .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) .destroyEngineWithFragment(shouldDestroyEngineWithHost()) .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) + .shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed) .build(); } else { Log.v( @@ -577,6 +579,7 @@ protected FlutterFragment createFlutterFragment() { .transparencyMode(transparencyMode) .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) + .shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed) .build(); } @@ -592,6 +595,7 @@ protected FlutterFragment createFlutterFragment() { .transparencyMode(transparencyMode) .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) + .shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed) .build(); } } @@ -616,12 +620,6 @@ protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); } - @Override - @SuppressWarnings("MissingSuperCall") - public void onBackPressed() { - flutterFragment.onBackPressed(); - } - @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java index 6cfc9e1c6009f..24d7c6ea32a5c 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -290,7 +290,7 @@ private FragmentActivity getMockFragmentActivity() { } @Test - public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() { + public void itDelegatesOnBackPressedWithSetFrameworkHandlesBack() { // We need to mock FlutterJNI to avoid triggering native code. FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); @@ -301,6 +301,8 @@ public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() { FlutterFragment fragment = FlutterFragment.withCachedEngine("my_cached_engine") + // This enables the use of onBackPressedCallback, which is what + // sends backs to the framework if setFrameworkHandlesBack is true. .shouldAutomaticallyHandleOnBackPressed(true) .build(); FragmentActivity activity = getMockFragmentActivity(); @@ -318,8 +320,15 @@ public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() { TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); fragment.setDelegateFactory(delegateFactory); + // Calling onBackPressed now will still be handled by Android (the default), + // until setFrameworkHandlesBack is set to true. activity.getOnBackPressedDispatcher().onBackPressed(); + verify(mockDelegate, times(0)).onBackPressed(); + // Setting setFrameworkHandlesBack to true means the delegate will receive + // the back and Android won't handle it. + fragment.setFrameworkHandlesBack(true); + activity.getOnBackPressedDispatcher().onBackPressed(); verify(mockDelegate, times(1)).onBackPressed(); } @@ -361,10 +370,20 @@ public void handleOnBackPressed() { TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); fragment.setDelegateFactory(delegateFactory); + assertTrue(callback.isEnabled()); + assertTrue(fragment.popSystemNavigator()); verify(mockDelegate, never()).onBackPressed(); assertTrue(onBackPressedCalled.get()); + assertTrue(callback.isEnabled()); + + callback.setEnabled(false); + assertFalse(callback.isEnabled()); + assertTrue(fragment.popSystemNavigator()); + + verify(mockDelegate, never()).onBackPressed(); + assertFalse(callback.isEnabled()); } @Test