7 #import <AudioToolbox/AudioToolbox.h>
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIApplication.h>
10 #import <UIKit/UIKit.h>
12 #include "flutter/fml/logging.h"
20 constexpr
char kTextPlainFormat[] =
"text/plain";
21 const UInt32 kKeyPressClickSoundId = 1306;
23 #if not APPLICATION_EXTENSION_API_ONLY
24 const NSString* searchURLPrefix =
@"x-web-search://?";
33 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
35 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
37 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
39 "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
46 #if not APPLICATION_EXTENSION_API_ONLY
47 [UIApplication sharedApplication].statusBarHidden = hidden;
49 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
54 #if not APPLICATION_EXTENSION_API_ONLY
57 [[UIApplication sharedApplication] setStatusBarStyle:style];
59 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
72 @property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
77 fml::WeakNSObject<FlutterEngine>
_engine;
82 - (instancetype)initWithEngine:(fml::WeakNSObject<
FlutterEngine>)engine {
83 FML_DCHECK(
engine) <<
"engine must be set";
88 NSObject* infoValue = [[NSBundle mainBundle]
89 objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
90 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
91 if (infoValue != nil && ![infoValue isKindOfClass:[NSNumber
class]]) {
92 FML_LOG(ERROR) <<
"The value of UIViewControllerBasedStatusBarAppearance in info.plist must "
96 _enableViewControllerBasedStatusBarAppearance =
97 (infoValue == nil || [(NSNumber*)infoValue boolValue]);
104 NSString* method = call.
method;
106 if ([method isEqualToString:
@"SystemSound.play"]) {
107 [
self playSystemSound:args];
109 }
else if ([method isEqualToString:
@"HapticFeedback.vibrate"]) {
110 [
self vibrateHapticFeedback:args];
112 }
else if ([method isEqualToString:
@"SystemChrome.setPreferredOrientations"]) {
113 [
self setSystemChromePreferredOrientations:args];
115 }
else if ([method isEqualToString:
@"SystemChrome.setApplicationSwitcherDescription"]) {
116 [
self setSystemChromeApplicationSwitcherDescription:args];
118 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIOverlays"]) {
119 [
self setSystemChromeEnabledSystemUIOverlays:args];
121 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIMode"]) {
122 [
self setSystemChromeEnabledSystemUIMode:args];
124 }
else if ([method isEqualToString:
@"SystemChrome.restoreSystemUIOverlays"]) {
125 [
self restoreSystemChromeSystemUIOverlays];
127 }
else if ([method isEqualToString:
@"SystemChrome.setSystemUIOverlayStyle"]) {
128 [
self setSystemChromeSystemUIOverlayStyle:args];
130 }
else if ([method isEqualToString:
@"SystemNavigator.pop"]) {
131 NSNumber* isAnimated = args;
132 [
self popSystemNavigator:isAnimated.boolValue];
134 }
else if ([method isEqualToString:
@"Clipboard.getData"]) {
135 result([
self getClipboardData:args]);
136 }
else if ([method isEqualToString:
@"Clipboard.setData"]) {
137 [
self setClipboardData:args];
139 }
else if ([method isEqualToString:
@"Clipboard.hasStrings"]) {
140 result([
self clipboardHasStrings]);
141 }
else if ([method isEqualToString:
@"LiveText.isLiveTextInputAvailable"]) {
142 result(@([
self isLiveTextInputAvailable]));
143 }
else if ([method isEqualToString:
@"SearchWeb.invoke"]) {
144 [
self searchWeb:args];
146 }
else if ([method isEqualToString:
@"LookUp.invoke"]) {
147 [
self showLookUpViewController:args];
149 }
else if ([method isEqualToString:
@"Share.invoke"]) {
150 [
self showShareViewController:args];
157 - (void)showShareViewController:(NSString*)content {
158 UIViewController* engineViewController = [_engine.get() viewController];
160 NSArray* itemsToShare = @[ content ?: [NSNull null] ];
161 UIActivityViewController* activityViewController =
162 [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare
163 applicationActivities:nil] autorelease];
165 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
174 caretRectForPosition:(FlutterTextPosition*)range.start];
176 localRectFromFrameworkTransform:firstRect];
178 caretRectForPosition:(FlutterTextPosition*)range.end];
180 localRectFromFrameworkTransform:lastRect];
182 activityViewController.popoverPresentationController.sourceView = engineViewController.view;
184 activityViewController.popoverPresentationController.sourceRect =
185 CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x),
186 transformedFirstRect.origin.y,
187 abs(transformedLastRect.origin.x - transformedFirstRect.origin.x),
188 transformedFirstRect.size.height);
191 [engineViewController presentViewController:activityViewController animated:YES completion:nil];
194 - (void)searchWeb:(NSString*)searchTerm {
195 #if APPLICATION_EXTENSION_API_ONLY
196 FML_LOG(WARNING) <<
"SearchWeb.invoke is not availabe in app extension.";
198 NSString* escapedText = [searchTerm
199 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
200 URLHostAllowedCharacterSet]];
201 NSString* searchURL = [NSString stringWithFormat:@"%@%@", searchURLPrefix, escapedText];
203 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL]
205 completionHandler:nil];
209 - (void)playSystemSound:(NSString*)soundType {
210 if ([soundType isEqualToString:
@"SystemSoundType.click"]) {
213 AudioServicesPlaySystemSound(kKeyPressClickSoundId);
217 - (void)vibrateHapticFeedback:(NSString*)feedbackType {
219 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
223 if ([
@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
224 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] autorelease]
226 }
else if ([
@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
227 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] autorelease]
229 }
else if ([
@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
230 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] autorelease]
232 }
else if ([
@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
233 [[[[UISelectionFeedbackGenerator alloc] init] autorelease] selectionChanged];
237 - (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
238 UIInterfaceOrientationMask mask = 0;
240 if (orientations.count == 0) {
241 mask |= UIInterfaceOrientationMaskAll;
243 for (NSString* orientation in orientations) {
244 if ([orientation isEqualToString:
@"DeviceOrientation.portraitUp"]) {
245 mask |= UIInterfaceOrientationMaskPortrait;
246 }
else if ([orientation isEqualToString:
@"DeviceOrientation.portraitDown"]) {
247 mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
248 }
else if ([orientation isEqualToString:
@"DeviceOrientation.landscapeLeft"]) {
249 mask |= UIInterfaceOrientationMaskLandscapeLeft;
250 }
else if ([orientation isEqualToString:
@"DeviceOrientation.landscapeRight"]) {
251 mask |= UIInterfaceOrientationMaskLandscapeRight;
259 [[NSNotificationCenter defaultCenter]
260 postNotificationName:@(kOrientationUpdateNotificationName)
262 userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
265 - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
269 - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
270 BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"];
271 if ([overlays containsObject:
@"SystemUiOverlay.bottom"]) {
272 [[NSNotificationCenter defaultCenter]
273 postNotificationName:FlutterViewControllerShowHomeIndicator
276 [[NSNotificationCenter defaultCenter]
277 postNotificationName:FlutterViewControllerHideHomeIndicator
280 if (
self.enableViewControllerBasedStatusBarAppearance) {
281 [_engine.get() viewController].prefersStatusBarHidden = statusBarShouldBeHidden;
293 - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
294 BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
295 if (
self.enableViewControllerBasedStatusBarAppearance) {
296 [_engine.get() viewController].prefersStatusBarHidden = !edgeToEdge;
306 [[NSNotificationCenter defaultCenter]
307 postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator
308 : FlutterViewControllerHideHomeIndicator
312 - (void)restoreSystemChromeSystemUIOverlays {
316 - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
317 NSString* brightness = message[@"statusBarBrightness"];
318 if (brightness == (
id)[NSNull
null]) {
322 UIStatusBarStyle statusBarStyle;
323 if ([brightness isEqualToString:
@"Brightness.dark"]) {
324 statusBarStyle = UIStatusBarStyleLightContent;
325 }
else if ([brightness isEqualToString:
@"Brightness.light"]) {
326 if (@available(iOS 13, *)) {
327 statusBarStyle = UIStatusBarStyleDarkContent;
329 statusBarStyle = UIStatusBarStyleDefault;
335 if (
self.enableViewControllerBasedStatusBarAppearance) {
337 [[NSNotificationCenter defaultCenter]
338 postNotificationName:@(kOverlayStyleUpdateNotificationName)
340 userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
346 - (void)popSystemNavigator:(BOOL)isAnimated {
354 UINavigationController* navigationController = [engineViewController navigationController];
355 if (navigationController) {
356 [navigationController popViewControllerAnimated:isAnimated];
358 UIViewController* rootViewController = nil;
359 #if APPLICATION_EXTENSION_API_ONLY
360 if (@available(iOS 15.0, *)) {
362 [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController;
365 <<
"rootViewController is not available in application extension prior to iOS 15.0.";
368 rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
370 if (engineViewController != rootViewController) {
371 [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
376 - (NSDictionary*)getClipboardData:(NSString*)format {
377 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
378 if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
379 NSString* stringInPasteboard = pasteboard.string;
381 return stringInPasteboard == nil ? nil : @{
@"text" : stringInPasteboard};
386 - (void)setClipboardData:(NSDictionary*)data {
387 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
388 id copyText = data[@"text"];
389 if ([copyText isKindOfClass:[NSString
class]]) {
390 pasteboard.string = copyText;
392 pasteboard.string =
@"null";
396 - (NSDictionary*)clipboardHasStrings {
397 return @{
@"value" : @([UIPasteboard generalPasteboard].hasStrings)};
400 - (BOOL)isLiveTextInputAvailable {
401 return [[
self textField] canPerformAction:@selector(captureTextFromCamera:) withSender:nil];
404 - (void)showLookUpViewController:(NSString*)term {
405 UIViewController* engineViewController = [_engine.get() viewController];
406 UIReferenceLibraryViewController* referenceLibraryViewController =
407 [[[UIReferenceLibraryViewController alloc] initWithTerm:term] autorelease];
408 [engineViewController presentViewController:referenceLibraryViewController
413 - (UITextField*)textField {
421 [_textField release];