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 all 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 @@ -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;
Expand Down Expand Up @@ -192,14 +193,25 @@ 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;
private int deferredInsetTypes;

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 +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<WindowInsetsAnimation> runningAnimations) {
if (!started) {
if (!animating || needsSave) {
return insets;
}
boolean matching = false;
Expand Down Expand Up @@ -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
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