From 7edfa8413b3d59b426583b6542c4400c676a874a Mon Sep 17 00:00:00 2001 From: maRci002 Date: Tue, 12 Mar 2024 13:11:48 +0100 Subject: [PATCH 1/6] API 34+ use OnBackAnimationCallback --- ci/licenses_golden/licenses_flutter | 2 + shell/platform/android/BUILD.gn | 1 + .../embedding/android/FlutterActivity.java | 85 ++++++++++-- .../FlutterActivityAndFragmentDelegate.java | 92 +++++++++++++ .../embedding/engine/FlutterEngine.java | 9 ++ .../systemchannels/BackGestureChannel.java | 127 ++++++++++++++++++ .../android/io/flutter/view/FlutterView.java | 28 ++++ ...lutterActivityAndFragmentDelegateTest.java | 70 ++++++++++ .../android/FlutterActivityTest.java | 109 ++++++++++++++- 9 files changed, 508 insertions(+), 15 deletions(-) create mode 100644 shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java 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..fb21fa92a4102 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,93 @@ void onBackPressed() { } } + /** + * Invoke this from {@link OnBackAnimationCallback#onBackStarted(BackEvent)}. + * + *

This method should be called when the back gesture is initiated, as part of the + * implementation of {@link OnBackAnimationCallback}. It's responsible for handling the initial + * response to the start of a back gesture, 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}. It allows for updating the state of the + * application or UI elements based on the ongoing back gesture, such as progressing animations or + * interactive elements. + * + * @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 signifies the completion of a back gesture and commits the navigation action + * initiated by the gesture. It should be called as a final step in the back gesture handling, + * indicating that the application is ready to proceed with the back navigation. This could + * involve finalizing animations, updating the UI to reflect the navigation, or performing cleanup + * tasks related to the gesture. + */ + @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 back button pressed, as + * part of the implementation of {@link OnBackAnimationCallback}. It's responsible for handling + * the rollback of any changes or animations that were initiated in response to the back gesture. + * This includes resetting UI elements or state to their original form before the gesture started. + */ + @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..4d9daae3ce2da --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -0,0 +1,127 @@ +// 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.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +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", JSONMethodCodec.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<>(4); + message.put("touchX", Float.isNaN(backEvent.getTouchX()) ? null : backEvent.getTouchX()); + message.put("touchY", Float.isNaN(backEvent.getTouchY()) ? null : backEvent.getTouchY()); + 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 From 5ddbce4f666a2799cb862790a45889bab4488aa6 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 16 Mar 2024 22:37:05 +0100 Subject: [PATCH 2/6] rename BackEvent touchX -> x and touchY -> y --- .../embedding/engine/systemchannels/BackGestureChannel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java index 4d9daae3ce2da..d7c08e969cea6 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -117,8 +117,8 @@ public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handl @RequiresApi(API_LEVELS.API_34) private Map backEventToJsonMap(@NonNull BackEvent backEvent) { Map message = new HashMap<>(4); - message.put("touchX", Float.isNaN(backEvent.getTouchX()) ? null : backEvent.getTouchX()); - message.put("touchY", Float.isNaN(backEvent.getTouchY()) ? null : backEvent.getTouchY()); + message.put("x", Float.isNaN(backEvent.getTouchX()) ? null : backEvent.getTouchX()); + message.put("y", Float.isNaN(backEvent.getTouchY()) ? null : backEvent.getTouchY()); message.put("progress", backEvent.getProgress()); message.put("swipeEdge", backEvent.getSwipeEdge()); From 4941baea1133b222cd5eea7ec5954707d7a94b35 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Tue, 19 Mar 2024 23:11:33 +0100 Subject: [PATCH 3/6] update doc --- .../FlutterActivityAndFragmentDelegate.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index fb21fa92a4102..2f1156704b982 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -787,10 +787,12 @@ void onBackPressed() { /** * Invoke this from {@link OnBackAnimationCallback#onBackStarted(BackEvent)}. * - *

This method should be called when the back gesture is initiated, as part of the - * implementation of {@link OnBackAnimationCallback}. It's responsible for handling the initial - * response to the start of a back gesture, such as initiating animations or preparing the UI for - * the back navigation process. + *

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. */ @@ -810,9 +812,10 @@ void startBackGesture(@NonNull BackEvent backEvent) { * 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}. It allows for updating the state of the - * application or UI elements based on the ongoing back gesture, such as progressing animations or - * interactive elements. + * 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. */ @@ -833,11 +836,13 @@ void updateBackGestureProgress(@NonNull BackEvent backEvent) { /** * Invoke this from {@link OnBackAnimationCallback#onBackInvoked()}. * - *

This method signifies the completion of a back gesture and commits the navigation action - * initiated by the gesture. It should be called as a final step in the back gesture handling, - * indicating that the application is ready to proceed with the back navigation. This could - * involve finalizing animations, updating the UI to reflect the navigation, or performing cleanup - * tasks related to the gesture. + *

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) @@ -854,10 +859,12 @@ void commitBackGesture() { /** * Invoke this from {@link OnBackAnimationCallback#onBackCancelled()}. * - *

This method should be called when a back gesture is cancelled or back button pressed, as - * part of the implementation of {@link OnBackAnimationCallback}. It's responsible for handling - * the rollback of any changes or animations that were initiated in response to the back gesture. - * This includes resetting UI elements or state to their original form before the gesture started. + *

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) From 6e421943d5708c7dd9952bf3d7b18e146877f842 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Wed, 20 Mar 2024 22:59:43 +0100 Subject: [PATCH 4/6] rename method channel to lowercase --- .../embedding/engine/systemchannels/BackGestureChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java index d7c08e969cea6..f5acb61fe0734 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -37,7 +37,7 @@ public class BackGestureChannel { * framework. */ public BackGestureChannel(@NonNull DartExecutor dartExecutor) { - this.channel = new MethodChannel(dartExecutor, "flutter/backGesture", JSONMethodCodec.INSTANCE); + this.channel = new MethodChannel(dartExecutor, "flutter/backgesture", JSONMethodCodec.INSTANCE); channel.setMethodCallHandler(defaultHandler); } From 939293234425baf809ff3303bdd1532d9c55fb1f Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 23 Mar 2024 00:51:05 +0100 Subject: [PATCH 5/6] use StandardMethodCodec / Offset --- .../engine/systemchannels/BackGestureChannel.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java index f5acb61fe0734..eb08a085c7c83 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -12,9 +12,10 @@ import io.flutter.Build.API_LEVELS; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.plugin.common.JSONMethodCodec; 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; @@ -37,7 +38,7 @@ public class BackGestureChannel { * framework. */ public BackGestureChannel(@NonNull DartExecutor dartExecutor) { - this.channel = new MethodChannel(dartExecutor, "flutter/backgesture", JSONMethodCodec.INSTANCE); + this.channel = new MethodChannel(dartExecutor, "flutter/backgesture", StandardMethodCodec.INSTANCE); channel.setMethodCallHandler(defaultHandler); } @@ -116,9 +117,13 @@ public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handl @TargetApi(API_LEVELS.API_34) @RequiresApi(API_LEVELS.API_34) private Map backEventToJsonMap(@NonNull BackEvent backEvent) { - Map message = new HashMap<>(4); - message.put("x", Float.isNaN(backEvent.getTouchX()) ? null : backEvent.getTouchX()); - message.put("y", Float.isNaN(backEvent.getTouchY()) ? null : backEvent.getTouchY()); + 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()); From 0caacd8569a03db5d34967057894c4af08f3bdb0 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Wed, 27 Mar 2024 08:41:33 +0100 Subject: [PATCH 6/6] format --- .../engine/systemchannels/BackGestureChannel.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java index eb08a085c7c83..6fb8997d46b54 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/BackGestureChannel.java @@ -38,7 +38,8 @@ public class BackGestureChannel { * framework. */ public BackGestureChannel(@NonNull DartExecutor dartExecutor) { - this.channel = new MethodChannel(dartExecutor, "flutter/backgesture", StandardMethodCodec.INSTANCE); + this.channel = + new MethodChannel(dartExecutor, "flutter/backgesture", StandardMethodCodec.INSTANCE); channel.setMethodCallHandler(defaultHandler); } @@ -120,9 +121,7 @@ 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); + 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());