From 7ee4615b80d2c128be05e0af03facf7c19800200 Mon Sep 17 00:00:00 2001 From: LouiseHsu Date: Wed, 2 Aug 2023 13:43:39 -0700 Subject: [PATCH] Add Search Web to selection controls on iOS (flutter/engine#43324) In native iOS, users are able to select text and initiate a web search on the selected text. Specifically, this will launch a search using the users selected default search engine. Apple provides a custom url scheme in the form of "x-web-search://?[term]" which will automatically launch a search using the user's preferred browser in Safari. Additionally, this also correctly handles the country code top level domain eg. a user with a selected region in the UK with a preferred Google search engine will automatically have the .co.uk domain appended to the end of the search. This PR is the engine portion of the changes that will allow Search Web to be implemented This PR addresses https://github.com/flutter/flutter/issues/82907 More details are available in this [design doc](https://github.com/flutter/engine/pull/flutter.dev/go/add-missing-features-to-selection-controls) --- .../framework/Source/FlutterPlatformPlugin.mm | 15 +++++++++++ .../Source/FlutterPlatformPluginTest.mm | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index e3603810536..ab270725675 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -16,6 +16,7 @@ namespace { constexpr char kTextPlainFormat[] = "text/plain"; const UInt32 kKeyPressClickSoundId = 1306; +const NSString* searchURLPrefix = @"x-web-search://?"; } // namespace @@ -115,6 +116,9 @@ using namespace flutter; result([self clipboardHasStrings]); } else if ([method isEqualToString:@"LiveText.isLiveTextInputAvailable"]) { result(@([self isLiveTextInputAvailable])); + } else if ([method isEqualToString:@"SearchWeb.invoke"]) { + [self searchWeb:args]; + result(nil); } else if ([method isEqualToString:@"LookUp.invoke"]) { [self showLookUpViewController:args]; result(nil); @@ -123,6 +127,17 @@ using namespace flutter; } } +- (void)searchWeb:(NSString*)searchTerm { + NSString* escapedText = [searchTerm + stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet + URLHostAllowedCharacterSet]]; + NSString* searchURL = [NSString stringWithFormat:@"%@%@", searchURLPrefix, escapedText]; + + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL] + options:@{} + completionHandler:nil]; +} + - (void)playSystemSound:(NSString*)soundType { if ([soundType isEqualToString:@"SystemSoundType.click"]) { // All feedback types are specific to Android and are treated as equal on diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index a50fefe3357..6ccbd8dd19a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -17,6 +17,7 @@ @interface FlutterPlatformPlugin () - (BOOL)isLiveTextInputAvailable; +- (void)searchWeb:(NSString*)searchTerm; - (void)showLookUpViewController:(NSString*)term; @end @@ -27,6 +28,30 @@ @end @implementation FlutterPlatformPluginTest +- (void)testSearchWebInvoked { + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + [engine runWithEntrypoint:nil]; + + XCTestExpectation* invokeExpectation = + [self expectationWithDescription:@"Web search launched with search term"]; + + FlutterPlatformPlugin* plugin = + [[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease]; + FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"SearchWeb.invoke" + arguments:@"Test"]; + + FlutterResult result = ^(id result) { + OCMVerify([mockPlugin searchWeb:@"Test"]); + [invokeExpectation fulfill]; + }; + + [mockPlugin handleMethodCall:methodCall result:result]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} - (void)testLookUpCallInitiated { FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease];