diff --git a/DEPS b/DEPS
index 297af528b902e..3d56e8fd1dbc6 100644
--- a/DEPS
+++ b/DEPS
@@ -34,7 +34,7 @@ vars = {
# Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS.
# You can use //tools/dart/create_updated_flutter_deps.py to produce
# updated revision list of existing dependencies.
- 'dart_revision': '52130c19ca593b185ea9cf72b26b1d02455551ef',
+ 'dart_revision': '4215dca724fb80de592f51a6cdba51e7638d1723',
# WARNING: DO NOT EDIT MANUALLY
# The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py
diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party
index f5805269c9196..69672b2b72f6f 100644
--- a/ci/licenses_golden/licenses_third_party
+++ b/ci/licenses_golden/licenses_third_party
@@ -1,4 +1,4 @@
-Signature: a1bbcd05a2657658be7c5f38e0d366f4
+Signature: 52ed6d65d7e96daef749ee003a3463a0
UNUSED LICENSES:
diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
index 094b8ef1c53e8..14d7effe56c11 100644
--- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
+++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
@@ -70,6 +70,15 @@ public AndroidKeyProcessor(
this.keyEventChannel.setEventResponseHandler(eventResponder);
}
+ /**
+ * Detaches the key processor from the Flutter engine.
+ *
+ *
The AndroidKeyProcessor instance should not be used after calling this.
+ */
+ public void destroy() {
+ keyEventChannel.setEventResponseHandler(null);
+ }
+
/**
* Called when a key up event is received by the {@link FlutterView}.
*
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index 2f1942034a7e5..c5c92a05d9dc3 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -992,6 +992,8 @@ public void detachFromFlutterEngine() {
textInputPlugin.getInputMethodManager().restartInput(this);
textInputPlugin.destroy();
+ androidKeyProcessor.destroy();
+
if (mouseCursorPlugin != null) {
mouseCursorPlugin.destroy();
}
diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index c11d264c21988..3090c6fba81fa 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -28,6 +28,7 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
+import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -192,6 +193,7 @@ public void sendAppPrivateCommand(String action, Bundle data) {
@TargetApi(30)
@RequiresApi(30)
@SuppressLint({"NewApi", "Override"})
+ @Keep
class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
implements View.OnApplyWindowInsetsListener {
private int overlayInsetTypes;
@@ -199,7 +201,17 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
private View view;
private WindowInsets lastWindowInsets;
- private boolean started = false;
+ // True when an animation that matches deferredInsetTypes is active.
+ //
+ // While this is active, this class will capture the initial window inset
+ // sent into lastWindowInsets by flagging needsSave to true, and will hold
+ // onto the intitial inset until the animation is completed, when it will
+ // re-dispatch the inset change.
+ private boolean animating = false;
+ // When an animation begins, android sends a WindowInset with the final
+ // state of the animation. When needsSave is true, we know to capture this
+ // initial WindowInset.
+ private boolean needsSave = false;
ImeSyncDeferringInsetsCallback(
@NonNull View view, int overlayInsetTypes, int deferredInsetTypes) {
@@ -212,34 +224,38 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
this.view = view;
- if (started) {
+ if (needsSave) {
+ // Store the view and insets for us in onEnd() below. This captured inset
+ // is not part of the animation and instead, represents the final state
+ // of the inset after the animation is completed. Thus, we defer the processing
+ // of this WindowInset until the animation completes.
+ lastWindowInsets = windowInsets;
+ needsSave = false;
+ }
+ if (animating) {
// While animation is running, we consume the insets to prevent disrupting
// the animation, which skips this implementation and calls the view's
// onApplyWindowInsets directly to avoid being consumed here.
return WindowInsets.CONSUMED;
}
- // Store the view and insets for us in onEnd() below
- lastWindowInsets = windowInsets;
-
// If no animation is happening, pass the insets on to the view's own
// inset handling.
return view.onApplyWindowInsets(windowInsets);
}
@Override
- public WindowInsetsAnimation.Bounds onStart(
- WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) {
+ public void onPrepare(WindowInsetsAnimation animation) {
if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
- started = true;
+ animating = true;
+ needsSave = true;
}
- return bounds;
}
@Override
public WindowInsets onProgress(
WindowInsets insets, List runningAnimations) {
- if (!started) {
+ if (!animating || needsSave) {
return insets;
}
boolean matching = false;
@@ -280,10 +296,10 @@ public WindowInsets onProgress(
@Override
public void onEnd(WindowInsetsAnimation animation) {
- if (started && (animation.getTypeMask() & deferredInsetTypes) != 0) {
+ if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
// If we deferred the IME insets and an IME animation has finished, we need to reset
// the flags
- started = false;
+ animating = false;
// And finally dispatch the deferred insets to the view now.
// Ideally we would just call view.requestApplyInsets() and let the normal dispatch
diff --git a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java
index 23369a2d3109b..8eddb009dc828 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java
@@ -2,7 +2,9 @@
import static junit.framework.TestCase.assertEquals;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.notNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -56,6 +58,22 @@ public void respondsTrueWhenHandlingNewEvents() {
verify(fakeView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
}
+ @Test
+ public void destroyTest() {
+ FlutterEngine flutterEngine = mockFlutterEngine();
+ KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
+ View fakeView = mock(View.class);
+
+ AndroidKeyProcessor processor =
+ new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
+
+ verify(fakeKeyEventChannel, times(1))
+ .setEventResponseHandler(notNull(KeyEventChannel.EventResponseHandler.class));
+ processor.destroy();
+ verify(fakeKeyEventChannel, times(1))
+ .setEventResponseHandler(isNull(KeyEventChannel.EventResponseHandler.class));
+ }
+
public void synthesizesEventsWhenKeyDownNotHandled() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
index 562f1f51dcf13..b038eb56000f3 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
@@ -669,6 +669,8 @@ public void ime_windowInsetsSync() {
WindowInsets.Builder builder = new WindowInsets.Builder();
WindowInsets noneInsets = builder.build();
+ // imeInsets0, 1, and 2 contain unique IME bottom insets, and are used
+ // to distinguish which insets were sent at each stage.
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
WindowInsets imeInsets0 = builder.build();
@@ -677,6 +679,10 @@ public void ime_windowInsetsSync() {
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
WindowInsets imeInsets1 = builder.build();
+ builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50));
+ builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
+ WindowInsets imeInsets2 = builder.build();
+
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 200));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 0));
WindowInsets deferredInsets = builder.build();
@@ -696,6 +702,8 @@ public void ime_windowInsetsSync() {
imeSyncCallback.onPrepare(animation);
imeSyncCallback.onApplyWindowInsets(testView, deferredInsets);
imeSyncCallback.onStart(animation, null);
+ // Only the final state call is saved, extra calls are passed on.
+ imeSyncCallback.onApplyWindowInsets(testView, imeInsets2);
verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
// No change, as deferredInset is stored to be passed in onEnd()
@@ -723,7 +731,7 @@ public void ime_windowInsetsSync() {
imeSyncCallback.onEnd(animation);
verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
- // Values should be of deferredInsets
+ // Values should be of deferredInsets, not imeInsets2
assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom);
assertEquals(10, viewportMetricsCaptor.getValue().paddingTop);
assertEquals(200, viewportMetricsCaptor.getValue().viewInsetBottom);
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
index 0179debf2c5a4..53cbff2202720 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
@@ -90,6 +90,15 @@ - (instancetype)initWithFrame:(CGRect)frame {
return self;
}
+// In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added
+// this view as a subview of the ChildClippingView.
+// This results this view blocking touch events on the ChildClippingView.
+// So we should always ignore any touch events sent to this view.
+// See https://github.com/flutter/flutter/issues/66044
+- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
+ return NO;
+}
+
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
index ba7a8b5ca9471..792256082fbf7 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
@@ -326,7 +326,11 @@ - (void)pushRoute:(NSString*)route {
auto placeholder = [[[UIView alloc] init] autorelease];
placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- placeholder.backgroundColor = UIColor.whiteColor;
+ if (@available(iOS 13.0, *)) {
+ placeholder.backgroundColor = UIColor.systemBackgroundColor;
+ } else {
+ placeholder.backgroundColor = UIColor.whiteColor;
+ }
placeholder.autoresizesSubviews = YES;
// Only add the label when we know we have failed to enable tracing (and it was necessary).
@@ -339,9 +343,9 @@ - (void)pushRoute:(NSString*)route {
messageLabel.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
messageLabel.text =
- @"In iOS 14+, Flutter application in debug mode can only be launched from Flutter tooling, "
+ @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
@"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
- @"modes to enable re-launching from the home screen.";
+ @"modes to enable launching from the home screen.";
[placeholder addSubview:messageLabel];
}
diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
index d09cc4db033c4..b981938a05bfc 100644
--- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
+++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
@@ -23,7 +23,9 @@ @implementation AppDelegate
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
-
+ if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--maskview-blocking"]) {
+ self.window.tintColor = UIColor.systemPinkColor;
+ }
NSDictionary* launchArgsMap = @{
// The Platform view golden test args should match `PlatformViewGoldenTestManager`.
@"--locale-initialization" : @"locale_initialization",
@@ -58,7 +60,6 @@ - (BOOL)application:(UIApplication*)application
*stop = YES;
}
}];
-
if (flutterViewControllerTestName) {
[self setupFlutterViewControllerTest:flutterViewControllerTestName];
} else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) {
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m
index 3d583e1d5e824..6771454c40318 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m
@@ -110,9 +110,54 @@ - (void)testAccept {
[[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:platformView];
[platformView tap];
+
+ [self waitForExpectations:@[ expection ] timeout:kSecondsToWaitForPlatformView];
+ XCTAssertEqualObjects(platformView.label,
+ @"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped");
+}
+
+- (void)testGestureWithMaskViewBlockingPlatformView {
+ XCUIApplication* app = [[XCUIApplication alloc] init];
+ app.launchArguments = @[ @"--gesture-accept", @"--maskview-blocking" ];
+ [app launch];
+
+ NSPredicate* predicateToFindPlatformView =
+ [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
+ NSDictionary* _Nullable bindings) {
+ XCUIElement* element = evaluatedObject;
+ return [element.identifier hasPrefix:@"platform_view"];
+ }];
+ XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView];
+ if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) {
+ NSLog(@"%@", app.debugDescription);
+ XCTFail(@"Failed due to not able to find any platformView with %@ seconds",
+ @(kSecondsToWaitForPlatformView));
+ }
+
+ XCTAssertNotNil(platformView);
+ XCTAssertEqualObjects(platformView.label, @"");
+
+ NSPredicate* predicate = [NSPredicate
+ predicateWithFormat:@"label == %@",
+ @"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped"];
+ XCTNSPredicateExpectation* expection =
+ [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:platformView];
+
+ XCUICoordinate* coordinate =
+ [self getNormalizedCoordinate:app
+ point:CGVectorMake(platformView.frame.origin.x + 10,
+ platformView.frame.origin.y + 10)];
+ [coordinate tap];
+
[self waitForExpectations:@[ expection ] timeout:kSecondsToWaitForPlatformView];
XCTAssertEqualObjects(platformView.label,
@"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped");
}
+- (XCUICoordinate*)getNormalizedCoordinate:(XCUIApplication*)app point:(CGVector)vector {
+ XCUICoordinate* appZero = [app coordinateWithNormalizedOffset:CGVectorMake(0, 0)];
+ XCUICoordinate* coordinate = [appZero coordinateWithOffset:vector];
+ return coordinate;
+}
+
@end
diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart
index e2d0d6a0e3d7b..90fc0693e0433 100644
--- a/testing/scenario_app/lib/src/platform_view.dart
+++ b/testing/scenario_app/lib/src/platform_view.dart
@@ -336,9 +336,9 @@ class MultiPlatformViewBackgroundForegroundScenario extends Scenario with _BaseP
MultiPlatformViewBackgroundForegroundScenario(Window window, {this.firstId, this.secondId})
: assert(window != null),
super(window) {
+ _nextFrame = _firstFrame;
createPlatformView(window, 'platform view 1', firstId);
createPlatformView(window, 'platform view 2', secondId);
- _nextFrame = _firstFrame;
}
/// The platform view identifier to use for the first platform view.
@@ -532,6 +532,8 @@ class PlatformViewForTouchIOSScenario extends Scenario
int _viewId;
bool _accept;
+
+ VoidCallback _nextFrame;
/// Creates the PlatformView scenario.
///
/// The [window] parameter must not be null.
@@ -545,14 +547,24 @@ class PlatformViewForTouchIOSScenario extends Scenario
} else {
createPlatformView(window, text, id);
}
+ _nextFrame = _firstFrame;
}
@override
void onBeginFrame(Duration duration) {
- final SceneBuilder builder = SceneBuilder();
+ _nextFrame();
+ }
- builder.pushOffset(0, 0);
- finishBuilderByAddingPlatformViewAndPicture(builder, _viewId);
+ @override
+ void onDrawFrame() {
+ // Some iOS gesture recognizers bugs are introduced in the second frame (with a different platform view rect) after laying out the platform view.
+ // So in this test, we load 2 frames to ensure that we cover those cases.
+ // See https://github.com/flutter/flutter/issues/66044
+ if (_nextFrame == _firstFrame) {
+ _nextFrame = _secondFrame;
+ window.scheduleFrame();
+ }
+ super.onDrawFrame();
}
@override
@@ -585,6 +597,20 @@ class PlatformViewForTouchIOSScenario extends Scenario
}
}
+
+ void _firstFrame() {
+ final SceneBuilder builder = SceneBuilder();
+
+ builder.pushOffset(0, 0);
+ finishBuilderByAddingPlatformViewAndPicture(builder, _viewId);
+ }
+
+ void _secondFrame() {
+ final SceneBuilder builder = SceneBuilder();
+
+ builder.pushOffset(5, 5);
+ finishBuilderByAddingPlatformViewAndPicture(builder, _viewId);
+ }
}
mixin _BasePlatformViewScenarioMixin on Scenario {