Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ci/licenses_golden/licenses_third_party
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Signature: a1bbcd05a2657658be7c5f38e0d366f4
Signature: 52ed6d65d7e96daef749ee003a3463a0

UNUSED LICENSES:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ public AndroidKeyProcessor(
this.keyEventChannel.setEventResponseHandler(eventResponder);
}

/**
* Detaches the key processor from the Flutter engine.
*
* <p>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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,8 @@ public void detachFromFlutterEngine() {
textInputPlugin.getInputMethodManager().restartInput(this);
textInputPlugin.destroy();

androidKeyProcessor.destroy();

if (mouseCursorPlugin != null) {
mouseCursorPlugin.destroy();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,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) {
Expand All @@ -212,34 +222,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<WindowInsetsAnimation> runningAnimations) {
if (!started) {
if (!animating || needsSave) {
return insets;
}
boolean matching = false;
Expand Down Expand Up @@ -280,10 +294,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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()
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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];
}

Expand Down
5 changes: 3 additions & 2 deletions testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString*, NSString*>* launchArgsMap = @{
// The Platform view golden test args should match `PlatformViewGoldenTestManager`.
@"--locale-initialization" : @"locale_initialization",
Expand Down Expand Up @@ -58,7 +60,6 @@ - (BOOL)application:(UIApplication*)application
*stop = YES;
}
}];

if (flutterViewControllerTestName) {
[self setupFlutterViewControllerTest:flutterViewControllerTestName];
} else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString*, id>* _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
34 changes: 30 additions & 4 deletions testing/scenario_app/lib/src/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down