diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index b6d82a2305b12..feb7ecc75b01c 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -40968,6 +40968,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/rend ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java + ../../../flutter/LICENSE @@ -43854,6 +43855,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/render FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureSurfaceProducer.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 3025fabbbbbec..b1a0f428e860b 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -271,6 +271,7 @@ android_java_sources = [ "io/flutter/embedding/engine/renderer/SurfaceTextureSurfaceProducer.java", "io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java", "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java", + "io/flutter/embedding/engine/systemchannels/BackGestureChannel.java", "io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java", "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", "io/flutter/embedding/engine/systemchannels/KeyboardChannel.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index dcecd3fbc1bd2..d536fed70dd06 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -22,6 +22,7 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -35,10 +36,13 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -678,18 +682,43 @@ public void unregisterOnBackInvokedCallback() { } private final OnBackInvokedCallback onBackInvokedCallback = - Build.VERSION.SDK_INT >= API_LEVELS.API_33 - ? new OnBackInvokedCallback() { - // TODO(garyq): Remove SuppressWarnings annotation. This was added to workaround - // a google3 bug where the linter is not properly running against API 33, causing - // a failure here. See b/243609613 and https://github.com/flutter/flutter/issues/111295 - @SuppressWarnings("Override") - @Override - public void onBackInvoked() { - onBackPressed(); - } - } - : null; + Build.VERSION.SDK_INT < API_LEVELS.API_33 ? null : createOnBackInvokedCallback(); + + @VisibleForTesting + protected OnBackInvokedCallback getOnBackInvokedCallback() { + return onBackInvokedCallback; + } + + @NonNull + @TargetApi(API_LEVELS.API_33) + @RequiresApi(API_LEVELS.API_33) + private OnBackInvokedCallback createOnBackInvokedCallback() { + if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { + return new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + commitBackGesture(); + } + + @Override + public void onBackCancelled() { + cancelBackGesture(); + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + updateBackGestureProgress(backEvent); + } + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + startBackGesture(backEvent); + } + }; + } + + return this::onBackPressed; + } @Override public void setFrameworkHandlesBack(boolean frameworkHandlesBack) { @@ -899,6 +928,38 @@ public void onBackPressed() { } } + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void startBackGesture(@NonNull BackEvent backEvent) { + if (stillAttachedForEvent("startBackGesture")) { + delegate.startBackGesture(backEvent); + } + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void updateBackGestureProgress(@NonNull BackEvent backEvent) { + if (stillAttachedForEvent("updateBackGestureProgress")) { + delegate.updateBackGestureProgress(backEvent); + } + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void commitBackGesture() { + if (stillAttachedForEvent("commitBackGesture")) { + delegate.commitBackGesture(); + } + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void cancelBackGesture() { + if (stillAttachedForEvent("cancelBackGesture")) { + delegate.cancelBackGesture(); + } + } + @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 6b8b0c44aafde..2f1156704b982 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -7,6 +7,7 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -16,10 +17,14 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnPreDrawListener; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; +import io.flutter.Build.API_LEVELS; import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; @@ -779,6 +784,100 @@ void onBackPressed() { } } + /** + * Invoke this from {@link OnBackAnimationCallback#onBackStarted(BackEvent)}. + * + *

This method should be called when the back gesture is initiated. It should be invoked as + * part of the implementation of {@link OnBackAnimationCallback}. + * + *

This method delegates the handling of the start of a back gesture to the Flutter framework, + * which is responsible for the appropriate response, such as initiating animations or preparing + * the UI for the back navigation process. + * + * @param backEvent The BackEvent object containing information about the touch. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + void startBackGesture(@NonNull BackEvent backEvent) { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding startBackGesture() to FlutterEngine."); + flutterEngine.getBackGestureChannel().startBackGesture(backEvent); + } else { + Log.w(TAG, "Invoked startBackGesture() before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link OnBackAnimationCallback#onBackProgressed(BackEvent)}. + * + *

This method should be called in response to progress in a back gesture, as part of the + * implementation of {@link OnBackAnimationCallback}. + * + *

This method delegates to the Flutter framework to update UI elements or animations based on + * the progression of the back gesture. + * + * @param backEvent An BackEvent object describing the progress event. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + void updateBackGestureProgress(@NonNull BackEvent backEvent) { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding updateBackGestureProgress() to FlutterEngine."); + flutterEngine.getBackGestureChannel().updateBackGestureProgress(backEvent); + } else { + Log.w( + TAG, + "Invoked updateBackGestureProgress() before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link OnBackAnimationCallback#onBackInvoked()}. + * + *

This method is called to signify the completion of a back gesture and commits the navigation + * action initiated by the gesture. It should be invoked as the final step in handling a back + * gesture. + * + *

This method indicates to the Flutter framework that it should proceed with the back + * navigation, including finalizing animations and updating the UI to reflect the navigation + * outcome. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + void commitBackGesture() { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding commitBackGesture() to FlutterEngine."); + flutterEngine.getBackGestureChannel().commitBackGesture(); + } else { + Log.w(TAG, "Invoked commitBackGesture() before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link OnBackAnimationCallback#onBackCancelled()}. + * + *

This method should be called when a back gesture is cancelled or the back button is pressed. + * It informs the Flutter framework about the cancellation. + * + *

This method enables the Flutter framework to rollback any UI changes or animations initiated + * in response to the back gesture. This includes resetting UI elements to their state prior to + * the gesture's start. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + void cancelBackGesture() { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding cancelBackGesture() to FlutterEngine."); + flutterEngine.getBackGestureChannel().cancelBackGesture(); + } else { + Log.w(TAG, "Invoked cancelBackGesture() before FlutterFragment was attached to an Activity."); + } + } + /** * Invoke this from {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])} * or {@code Fragment#onRequestPermissionsResult(int, String[], int[])}. diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 7ff6830c7bba5..4c80b90a603f7 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -25,6 +25,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.BackGestureChannel; import io.flutter.embedding.engine.systemchannels.DeferredComponentChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; @@ -95,6 +96,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater { @NonNull private final LocalizationChannel localizationChannel; @NonNull private final MouseCursorChannel mouseCursorChannel; @NonNull private final NavigationChannel navigationChannel; + @NonNull private final BackGestureChannel backGestureChannel; @NonNull private final RestorationChannel restorationChannel; @NonNull private final PlatformChannel platformChannel; @NonNull private final ProcessTextChannel processTextChannel; @@ -331,6 +333,7 @@ public FlutterEngine( localizationChannel = new LocalizationChannel(dartExecutor); mouseCursorChannel = new MouseCursorChannel(dartExecutor); navigationChannel = new NavigationChannel(dartExecutor); + backGestureChannel = new BackGestureChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager()); restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData); @@ -541,6 +544,12 @@ public NavigationChannel getNavigationChannel() { return navigationChannel; } + /** System channel that sends back gesture commands from Android to Flutter. */ + @NonNull + public BackGestureChannel getBackGestureChannel() { + return backGestureChannel; + } + /** * System channel that sends platform-oriented requests and information to Flutter, e.g., requests * to play sounds, requests for haptics, system chrome settings, etc. diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java new file mode 100644 index 0000000000000..6fb8997d46b54 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.systemchannels; + +import android.annotation.TargetApi; +import android.window.BackEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import io.flutter.Build.API_LEVELS; +import io.flutter.Log; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MethodChannel} for communicating back gesture events to the Flutter framework. + * + *

The BackGestureChannel facilitates communication between the platform-specific Android back + * gesture handling code and the Flutter framework. It enables the dispatch of back gesture events + * such as start, progress, commit, and cancellation from the platform to the Flutter application. + */ +public class BackGestureChannel { + private static final String TAG = "BackGestureChannel"; + + @NonNull public final MethodChannel channel; + + /** + * Constructs a BackGestureChannel. + * + * @param dartExecutor The DartExecutor used to establish communication with the Flutter + * framework. + */ + public BackGestureChannel(@NonNull DartExecutor dartExecutor) { + this.channel = + new MethodChannel(dartExecutor, "flutter/backgesture", StandardMethodCodec.INSTANCE); + channel.setMethodCallHandler(defaultHandler); + } + + // Provide a default handler that returns an empty response to any messages + // on this channel. + private final MethodChannel.MethodCallHandler defaultHandler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + result.success(null); + } + }; + + /** + * Initiates a back gesture event. + * + *

This method should be called when the back gesture is initiated by the user. + * + * @param backEvent The BackEvent object containing information about the touch. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void startBackGesture(@NonNull BackEvent backEvent) { + Log.v(TAG, "Sending message to start back gesture"); + channel.invokeMethod("startBackGesture", backEventToJsonMap(backEvent)); + } + + /** + * Updates the progress of a back gesture event. + * + *

This method should be called to update the progress of an ongoing back gesture event. + * + * @param backEvent An BackEvent object describing the progress event. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void updateBackGestureProgress(@NonNull BackEvent backEvent) { + Log.v(TAG, "Sending message to update back gesture progress"); + channel.invokeMethod("updateBackGestureProgress", backEventToJsonMap(backEvent)); + } + + /** + * Commits the back gesture event. + * + *

This method should be called to signify the completion of a back gesture event and commit + * the navigation action initiated by the gesture. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void commitBackGesture() { + Log.v(TAG, "Sending message to commit back gesture"); + channel.invokeMethod("commitBackGesture", null); + } + + /** + * Cancels the back gesture event. + * + *

This method should be called when a back gesture is cancelled or the back button is pressed. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void cancelBackGesture() { + Log.v(TAG, "Sending message to cancel back gesture"); + channel.invokeMethod("cancelBackGesture", null); + } + + /** + * Sets a method call handler for the channel. + * + * @param handler The handler to set for the channel. + */ + public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handler) { + channel.setMethodCallHandler(handler); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + private Map backEventToJsonMap(@NonNull BackEvent backEvent) { + Map message = new HashMap<>(3); + final float x = backEvent.getTouchX(); + final float y = backEvent.getTouchY(); + final Object touchOffset = (Float.isNaN(x) || Float.isNaN(y)) ? null : Arrays.asList(x, y); + message.put("touchOffset", touchOffset); + message.put("progress", backEvent.getProgress()); + message.put("swipeEdge", backEvent.getSwipeEdge()); + + return message; + } +} diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 73794cd46e41f..62f2a17505174 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -38,6 +38,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.window.BackEvent; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; @@ -49,6 +50,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.BackGestureChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; @@ -124,6 +126,7 @@ static final class ViewportMetrics { private final DartExecutor dartExecutor; private final FlutterRenderer flutterRenderer; private final NavigationChannel navigationChannel; + private final BackGestureChannel backGestureChannel; private final LifecycleChannel lifecycleChannel; private final LocalizationChannel localizationChannel; private final PlatformChannel platformChannel; @@ -214,6 +217,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { // Create all platform channels navigationChannel = new NavigationChannel(dartExecutor); + backGestureChannel = new BackGestureChannel(dartExecutor); lifecycleChannel = new LifecycleChannel(dartExecutor); localizationChannel = new LocalizationChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); @@ -369,6 +373,30 @@ public void popRoute() { navigationChannel.popRoute(); } + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void startBackGesture(@NonNull BackEvent backEvent) { + backGestureChannel.startBackGesture(backEvent); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void updateBackGestureProgress(@NonNull BackEvent backEvent) { + backGestureChannel.updateBackGestureProgress(backEvent); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void commitBackGesture() { + backGestureChannel.commitBackGesture(); + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + public void cancelBackGesture() { + backGestureChannel.cancelBackGesture(); + } + private void sendUserPlatformSettingsToDart() { // Lookup the current brightness of the Android OS. boolean isNightModeOn = diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 26a0a04098649..7262eaf0c37a3 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.net.Uri; import android.view.View; +import android.window.BackEvent; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ApplicationProvider; @@ -39,6 +40,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.BackGestureChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; @@ -671,6 +673,73 @@ public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() { verify(mockFlutterEngine.getNavigationChannel(), times(1)).popRoute(); } + @Test + public void itForwardsStartBackGestureToFlutter() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is set up in onAttach(). + delegate.onAttach(ctx); + + // Emulate the host and inform our delegate of the start back gesture with a mocked BackEvent + BackEvent backEvent = mock(BackEvent.class); + delegate.startBackGesture(backEvent); + + // Verify that the back gesture tried to send a message to Flutter. + verify(mockFlutterEngine.getBackGestureChannel(), times(1)).startBackGesture(backEvent); + } + + @Test + public void itForwardsUpdateBackGestureProgressToFlutter() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is set up in onAttach(). + delegate.onAttach(ctx); + + // Emulate the host and inform our delegate of the back gesture progress with a mocked BackEvent + BackEvent backEvent = mock(BackEvent.class); + delegate.updateBackGestureProgress(backEvent); + + // Verify that the back gesture tried to send a message to Flutter. + verify(mockFlutterEngine.getBackGestureChannel(), times(1)) + .updateBackGestureProgress(backEvent); + } + + @Test + public void itForwardsCommitBackGestureToFlutter() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is set up in onAttach(). + delegate.onAttach(ctx); + + // Emulate the host and inform our delegate when the back gesture is committed + delegate.commitBackGesture(); + + // Verify that the back gesture tried to send a message to Flutter. + verify(mockFlutterEngine.getBackGestureChannel(), times(1)).commitBackGesture(); + } + + @Test + public void itForwardsCancelBackGestureToFlutter() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is set up in onAttach(). + delegate.onAttach(ctx); + + // Emulate the host and inform our delegate of the back gesture cancellation + delegate.cancelBackGesture(); + + // Verify that the back gesture tried to send a message to Flutter. + verify(mockFlutterEngine.getBackGestureChannel(), times(1)).cancelBackGesture(); + } + @Test public void itForwardsOnRequestPermissionsResultToFlutterEngine() { // Create the real object that we're testing. @@ -1396,6 +1465,7 @@ private FlutterEngine mockFlutterEngine() { when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class)); when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class)); when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class)); + when(engine.getBackGestureChannel()).thenReturn(mock(BackGestureChannel.class)); when(engine.getPlatformViewsController()).thenReturn(mock(PlatformViewsController.class)); FlutterRenderer renderer = mock(FlutterRenderer.class); 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 d68564f2e2aa8..f660e32ebabdf 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -22,8 +22,12 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; @@ -122,6 +126,71 @@ public void itUnregistersOnBackInvokedCallbackOnRelease() { verify(activity, times(1)).unregisterOnBackInvokedCallback(); } + @Test + @Config(sdk = API_LEVELS.API_32) + public void onBackInvokedCallbackIsNullForSdk32OrLower() { + Intent intent = FlutterActivity.createDefaultIntent(ctx); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + assertNull( + "onBackInvokedCallback should be null for SDK 32 or lower", + flutterActivity.getOnBackInvokedCallback()); + } + + @Test + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + public void onBackInvokedCallbackCallsOnBackPressedForSdk33() { + Intent intent = FlutterActivityWithMockBackInvokedHandling.createDefaultIntent(ctx); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithMockBackInvokedHandling.class, intent); + FlutterActivityWithMockBackInvokedHandling activity = activityController.get(); + + OnBackInvokedCallback callback = activity.getOnBackInvokedCallback(); + assertNotNull("onBackInvokedCallback should not be null for SDK 33", callback); + + callback.onBackInvoked(); + assertEquals("Expected onBackPressed to be called 1 times", 1, activity.onBackPressedCounter); + } + + @Test + @Config(sdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + public void itHandlesOnBackAnimationCallbackAsExpectedForSdk34OrHigher() { + Intent intent = FlutterActivityWithMockBackInvokedHandling.createDefaultIntent(ctx); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithMockBackInvokedHandling.class, intent); + FlutterActivityWithMockBackInvokedHandling activity = activityController.get(); + + assertTrue( + "onBackInvokedCallback should be an instance of OnBackAnimationCallback for SDK 34 or higher", + activity.getOnBackInvokedCallback() instanceof OnBackAnimationCallback); + + OnBackAnimationCallback callback = + (OnBackAnimationCallback) activity.getOnBackInvokedCallback(); + + BackEvent mockBackEvent = mock(BackEvent.class); + callback.onBackStarted(mockBackEvent); + assertEquals( + "Expected startBackGesture to be called 1 times", 1, activity.startBackGestureCounter); + + callback.onBackProgressed(mockBackEvent); + assertEquals( + "Expected updateBackGestureProgress to be called 1 times", + 1, + activity.updateBackGestureProgressCounter); + + callback.onBackInvoked(); + assertEquals( + "Expected commitBackGesture to be called 1 times", 1, activity.commitBackGestureCounter); + + callback.onBackCancelled(); + assertEquals( + "Expected cancelBackGesture to be called 1 times", 1, activity.cancelBackGestureCounter); + } + @Test public void itCreatesDefaultIntentWithExpectedDefaults() { Intent intent = FlutterActivity.createDefaultIntent(ctx); @@ -568,12 +637,46 @@ public void resetFullyDrawn() { } } - private class FlutterActivityWithMockBackInvokedHandling extends FlutterActivity { + private static class FlutterActivityWithMockBackInvokedHandling extends FlutterActivity { + + int onBackPressedCounter = 0; + int startBackGestureCounter = 0; + int updateBackGestureProgressCounter = 0; + int commitBackGestureCounter = 0; + int cancelBackGestureCounter = 0; + + @Override + public void onBackPressed() { + onBackPressedCounter++; + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) @Override - public void registerOnBackInvokedCallback() {} + public void startBackGesture(@NonNull BackEvent backEvent) { + startBackGestureCounter++; + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + @Override + public void updateBackGestureProgress(@NonNull BackEvent backEvent) { + updateBackGestureProgressCounter++; + } + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) @Override - public void unregisterOnBackInvokedCallback() {} + public void commitBackGesture() { + commitBackGestureCounter++; + } + + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + @Override + public void cancelBackGesture() { + cancelBackGestureCounter++; + } } private static final class FakeFlutterPlugin