diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 13137ebf7dd7b..dd502a880c800 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -35,6 +35,8 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -495,6 +497,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + registerOnBackInvokedCallback(); + configureWindowForTransparency(); setContentView(createFlutterView()); @@ -502,6 +506,48 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { configureStatusBarForFullscreenFlutterExperience(); } + /** + * Registers the callback with OnBackInvokedDispatcher to capture back navigation gestures and + * pass them to the framework. + * + *

This replaces the deprecated onBackPressed method override in order to support API 33's + * predictive back navigation feature. + * + *

The callback must be unregistered in order to prevent unpredictable behavior once outside + * the Flutter app. + */ + @VisibleForTesting + public void registerOnBackInvokedCallback() { + if (Build.VERSION.SDK_INT >= 33) { + getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, onBackInvokedCallback); + } + } + + /** + * Unregisters the callback from OnBackInvokedDispatcher. + * + *

This should be called when the activity is no longer in use to prevent unpredictable + * behavior such as being stuck and unable to press back. + */ + @VisibleForTesting + public void unregisterOnBackInvokedCallback() { + if (Build.VERSION.SDK_INT >= 33) { + getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(onBackInvokedCallback); + } + } + + private final OnBackInvokedCallback onBackInvokedCallback = + Build.VERSION.SDK_INT >= 33 + ? new OnBackInvokedCallback() { + @Override + public void onBackInvoked() { + onBackPressed(); + } + } + : null; + /** * Switches themes for this {@code Activity} from the theme used to launch this {@code Activity} * to a "normal theme" that is intended for regular {@code Activity} operation. @@ -680,7 +726,9 @@ protected void onSaveInstanceState(Bundle outState) { * *

After calling, this activity should be disposed immediately and not be re-used. */ - private void release() { + @VisibleForTesting + public void release() { + unregisterOnBackInvokedCallback(); if (delegate != null) { delegate.release(); delegate = null; diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index d6d1d30d442f0..75d7abfcb4f03 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -88,6 +88,36 @@ public void flutterViewHasId() { assertTrue(activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID) instanceof FlutterView); } + // TODO(garyq): Robolectric does not yet support android api 33 yet. Switch to a robolectric + // test that directly exercises the OnBackInvoked APIs when API 33 is supported. + @Test + @TargetApi(33) + public void itRegistersOnBackInvokedCallbackOnCreate() { + Intent intent = FlutterActivityWithReportFullyDrawn.createDefaultIntent(ctx); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithReportFullyDrawn.class, intent); + FlutterActivityWithReportFullyDrawn activity = spy(activityController.get()); + + activity.onCreate(null); + + verify(activity, times(1)).registerOnBackInvokedCallback(); + } + + // TODO(garyq): Robolectric does not yet support android api 33 yet. Switch to a robolectric + // test that directly exercises the OnBackInvoked APIs when API 33 is supported. + @Test + @TargetApi(33) + public void itUnregistersOnBackInvokedCallbackOnRelease() { + Intent intent = FlutterActivityWithReportFullyDrawn.createDefaultIntent(ctx); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithReportFullyDrawn.class, intent); + FlutterActivityWithReportFullyDrawn activity = spy(activityController.get()); + + activity.release(); + + verify(activity, times(1)).unregisterOnBackInvokedCallback(); + } + @Test public void itCreatesDefaultIntentWithExpectedDefaults() { Intent intent = FlutterActivity.createDefaultIntent(ctx); @@ -596,6 +626,14 @@ public void resetFullyDrawn() { } } + private class FlutterActivityWithMockBackInvokedHandling extends FlutterActivity { + @Override + public void registerOnBackInvokedCallback() {} + + @Override + public void unregisterOnBackInvokedCallback() {} + } + private static final class FakeFlutterPlugin implements FlutterPlugin, ActivityAware, diff --git a/shell/platform/android/test_runner/build.gradle b/shell/platform/android/test_runner/build.gradle index dfdf361aeeae0..d1dfa4cc8d37b 100644 --- a/shell/platform/android/test_runner/build.gradle +++ b/shell/platform/android/test_runner/build.gradle @@ -71,10 +71,10 @@ android { testImplementation "com.google.android.play:core:1.8.0" testImplementation "com.ibm.icu:icu4j:69.1" testImplementation "org.robolectric:robolectric:4.7.3" - testImplementation "junit:junit:4.13" - testImplementation "androidx.test.ext:junit:1.1.3" + testImplementation "junit:junit:4.13.2" + testImplementation "androidx.test.ext:junit:1.1.4-alpha07" - def mockitoVersion = "4.1.0" + def mockitoVersion = "4.7.0" testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito:mockito-inline:$mockitoVersion" testImplementation "org.mockito:mockito-android:$mockitoVersion"