From 66a806ddd936565a8137666df4bc89a6918d2bc9 Mon Sep 17 00:00:00 2001 From: LouiseHsu Date: Wed, 22 Nov 2023 12:49:58 -0800 Subject: [PATCH 1/4] Fix Share Screen Crash on iPad (#48220) Fixes https://github.com/flutter/flutter/issues/138550 ![Simulator Screenshot - iPad Air (5th generation) - 2023-11-21 at 03 33 37](https://github.com/flutter/engine/assets/36148254/15e10e43-816b-43b1-a5ab-75c8add90899) --- .../framework/Source/FlutterPlatformPlugin.mm | 30 +++++++++++++++ .../Source/FlutterPlatformPluginTest.mm | 38 +++++++++++++++++++ .../framework/Source/FlutterTextInputPlugin.h | 4 ++ 3 files changed, 72 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 722571bda0a03..586f1c36b79f4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -10,6 +10,8 @@ #import #include "flutter/fml/logging.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" @@ -154,10 +156,38 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)showShareViewController:(NSString*)content { UIViewController* engineViewController = [_engine.get() viewController]; + NSArray* itemsToShare = @[ content ?: [NSNull null] ]; UIActivityViewController* activityViewController = [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare applicationActivities:nil] autorelease]; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // On iPad, the share screen is presented in a popover view, and requires a + // sourceView and sourceRect + FlutterTextInputPlugin* _textInputPlugin = [_engine.get() textInputPlugin]; + UITextRange* range = _textInputPlugin.textInputView.selectedTextRange; + + // firstRectForRange cannot be used here as it's current implementation does + // not always return the full rect of the range. + CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.start]; + CGRect transformedFirstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:firstRect]; + CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.end]; + CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:lastRect]; + + activityViewController.popoverPresentationController.sourceView = engineViewController.view; + // In case of RTL Language, get the minimum x coordinate + activityViewController.popoverPresentationController.sourceRect = + CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x), + transformedFirstRect.origin.y, + abs(transformedLastRect.origin.x - transformedFirstRect.origin.x), + transformedFirstRect.size.height); + } + [engineViewController presentViewController:activityViewController animated:YES completion:nil]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 73cc460b351f3..ed70b365e3892 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -161,6 +161,44 @@ - (void)testShareScreenInvoked { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testShareScreenInvokedOnIPad { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + [engine runWithEntrypoint:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + + XCTestExpectation* presentExpectation = + [self expectationWithDescription:@"Share view controller presented on iPad"]; + + FlutterViewController* engineViewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController); + OCMStub([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + + id mockTraitCollection = OCMClassMock([UITraitCollection class]); + OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); + + FlutterPlatformPlugin* plugin = + [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakNSObject()]; + FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"Share.invoke" + arguments:@"Test"]; + FlutterResult result = ^(id result) { + OCMVerify([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + [presentExpectation fulfill]; + }; + [mockPlugin handleMethodCall:methodCall result:result]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (void)testClipboardHasCorrectStrings { [UIPasteboard generalPasteboard].string = nil; FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h index 96eff563e0850..888fdaa898954 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -164,6 +164,10 @@ FLUTTER_DARWIN_EXPORT - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER; +// TODO(louisehsu): These are being exposed to support Share in FlutterPlatformPlugin +// Consider moving that feature into FlutterTextInputPlugin to avoid exposing extra methods +- (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect; +- (CGRect)caretRectForPosition:(UITextPosition*)position; @end @interface UIView (FindFirstResponder) From 7ff2e01ed3183ba8988d8e9b71468280edd3ba8c Mon Sep 17 00:00:00 2001 From: Louise Hsu Date: Mon, 27 Nov 2023 22:43:26 -0800 Subject: [PATCH 2/4] update type --- .../ios/framework/Source/FlutterPlatformPluginTest.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index ed70b365e3892..c8d9cb9987314 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -164,8 +164,8 @@ - (void)testShareScreenInvoked { - (void)testShareScreenInvokedOnIPad { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; [engine runWithEntrypoint:nil]; - std::unique_ptr> _weakFactory = - std::make_unique>(engine); + std::unique_ptr> _weakFactory = + std::make_unique>(engine); XCTestExpectation* presentExpectation = [self expectationWithDescription:@"Share view controller presented on iPad"]; @@ -183,7 +183,7 @@ - (void)testShareScreenInvokedOnIPad { OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); FlutterPlatformPlugin* plugin = - [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakNSObject()]; + [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->WeakPtrFactory()]; FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"Share.invoke" From ce012a43aa30c1db6631f51fe0fe2ed6e6b06a29 Mon Sep 17 00:00:00 2001 From: Louise Hsu Date: Mon, 27 Nov 2023 23:32:01 -0800 Subject: [PATCH 3/4] autorelease --- .../darwin/ios/framework/Source/FlutterPlatformPluginTest.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index c8d9cb9987314..a8746d8ffa69a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -183,7 +183,7 @@ - (void)testShareScreenInvokedOnIPad { OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); FlutterPlatformPlugin* plugin = - [[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->WeakPtrFactory()]; + [[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease]; FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"Share.invoke" From 99bf8ad81627ea9fe9968f547df4e0e8b6da6f02 Mon Sep 17 00:00:00 2001 From: Louise Hsu Date: Tue, 28 Nov 2023 12:19:45 -0800 Subject: [PATCH 4/4] adding autorelease --- .../ios/framework/Source/FlutterPlatformPluginTest.mm | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index a8746d8ffa69a..79e8089c42278 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -162,7 +162,7 @@ - (void)testShareScreenInvoked { } - (void)testShareScreenInvokedOnIPad { - FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; [engine runWithEntrypoint:nil]; std::unique_ptr> _weakFactory = std::make_unique>(engine); @@ -170,9 +170,8 @@ - (void)testShareScreenInvokedOnIPad { XCTestExpectation* presentExpectation = [self expectationWithDescription:@"Share view controller presented on iPad"]; - FlutterViewController* engineViewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; + FlutterViewController* engineViewController = + [[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease]; FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController); OCMStub([mockEngineViewController presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]]