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"
18 constexpr
char kTextPlainFormat[] =
"text/plain";
19 const UInt32 kKeyPressClickSoundId = 1306;
21 #if not APPLICATION_EXTENSION_API_ONLY
22 const NSString* searchURLPrefix =
@"x-web-search://?";
31 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
33 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
35 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
37 "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
44 #if APPLICATION_EXTENSION_API_ONLY
45 [UIApplication sharedApplication].statusBarHidden = hidden;
47 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
52 #if APPLICATION_EXTENSION_API_ONLY
55 [[UIApplication sharedApplication] setStatusBarStyle:style];
57 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
70 @property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
75 fml::WeakPtr<FlutterEngine>
_engine;
81 FML_DCHECK(
engine) <<
"engine must be set";
86 NSObject* infoValue = [[NSBundle mainBundle]
87 objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
88 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
89 if (infoValue != nil && ![infoValue isKindOfClass:[NSNumber
class]]) {
90 FML_LOG(ERROR) <<
"The value of UIViewControllerBasedStatusBarAppearance in info.plist must "
94 _enableViewControllerBasedStatusBarAppearance =
95 (infoValue == nil || [(NSNumber*)infoValue boolValue]);
102 NSString* method = call.
method;
104 if ([method isEqualToString:
@"SystemSound.play"]) {
105 [
self playSystemSound:args];
107 }
else if ([method isEqualToString:
@"HapticFeedback.vibrate"]) {
108 [
self vibrateHapticFeedback:args];
110 }
else if ([method isEqualToString:
@"SystemChrome.setPreferredOrientations"]) {
111 [
self setSystemChromePreferredOrientations:args];
113 }
else if ([method isEqualToString:
@"SystemChrome.setApplicationSwitcherDescription"]) {
114 [
self setSystemChromeApplicationSwitcherDescription:args];
116 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIOverlays"]) {
117 [
self setSystemChromeEnabledSystemUIOverlays:args];
119 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIMode"]) {
120 [
self setSystemChromeEnabledSystemUIMode:args];
122 }
else if ([method isEqualToString:
@"SystemChrome.restoreSystemUIOverlays"]) {
123 [
self restoreSystemChromeSystemUIOverlays];
125 }
else if ([method isEqualToString:
@"SystemChrome.setSystemUIOverlayStyle"]) {
126 [
self setSystemChromeSystemUIOverlayStyle:args];
128 }
else if ([method isEqualToString:
@"SystemNavigator.pop"]) {
129 NSNumber* isAnimated = args;
130 [
self popSystemNavigator:isAnimated.boolValue];
132 }
else if ([method isEqualToString:
@"Clipboard.getData"]) {
133 result([
self getClipboardData:args]);
134 }
else if ([method isEqualToString:
@"Clipboard.setData"]) {
135 [
self setClipboardData:args];
137 }
else if ([method isEqualToString:
@"Clipboard.hasStrings"]) {
138 result([
self clipboardHasStrings]);
139 }
else if ([method isEqualToString:
@"LiveText.isLiveTextInputAvailable"]) {
140 result(@([
self isLiveTextInputAvailable]));
141 }
else if ([method isEqualToString:
@"SearchWeb.invoke"]) {
142 [
self searchWeb:args];
144 }
else if ([method isEqualToString:
@"LookUp.invoke"]) {
145 [
self showLookUpViewController:args];
147 }
else if ([method isEqualToString:
@"Share.invoke"]) {
148 [
self showShareViewController:args];
155 - (void)showShareViewController:(NSString*)content {
156 UIViewController* engineViewController = [_engine.get() viewController];
157 NSArray* itemsToShare = @[ content ?: [NSNull null] ];
158 UIActivityViewController* activityViewController =
159 [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare
160 applicationActivities:nil] autorelease];
161 [engineViewController presentViewController:activityViewController animated:YES completion:nil];
164 - (void)searchWeb:(NSString*)searchTerm {
165 #if APPLICATION_EXTENSION_API_ONLY
166 FML_LOG(WARNING) <<
"SearchWeb.invoke is not availabe in app extension.";
168 NSString* escapedText = [searchTerm
169 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
170 URLHostAllowedCharacterSet]];
171 NSString* searchURL = [NSString stringWithFormat:@"%@%@", searchURLPrefix, escapedText];
173 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL]
175 completionHandler:nil];
179 - (void)playSystemSound:(NSString*)soundType {
180 if ([soundType isEqualToString:
@"SystemSoundType.click"]) {
183 AudioServicesPlaySystemSound(kKeyPressClickSoundId);
187 - (void)vibrateHapticFeedback:(NSString*)feedbackType {
189 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
193 if ([
@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
194 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] autorelease]
196 }
else if ([
@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
197 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] autorelease]
199 }
else if ([
@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
200 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] autorelease]
202 }
else if ([
@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
203 [[[[UISelectionFeedbackGenerator alloc] init] autorelease] selectionChanged];
207 - (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
208 UIInterfaceOrientationMask mask = 0;
210 if (orientations.count == 0) {
211 mask |= UIInterfaceOrientationMaskAll;
213 for (NSString* orientation in orientations) {
214 if ([orientation isEqualToString:
@"DeviceOrientation.portraitUp"]) {
215 mask |= UIInterfaceOrientationMaskPortrait;
216 }
else if ([orientation isEqualToString:
@"DeviceOrientation.portraitDown"]) {
217 mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
218 }
else if ([orientation isEqualToString:
@"DeviceOrientation.landscapeLeft"]) {
219 mask |= UIInterfaceOrientationMaskLandscapeLeft;
220 }
else if ([orientation isEqualToString:
@"DeviceOrientation.landscapeRight"]) {
221 mask |= UIInterfaceOrientationMaskLandscapeRight;
229 [[NSNotificationCenter defaultCenter]
230 postNotificationName:@(kOrientationUpdateNotificationName)
232 userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
235 - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
239 - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
240 BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"];
241 if ([overlays containsObject:
@"SystemUiOverlay.bottom"]) {
242 [[NSNotificationCenter defaultCenter]
243 postNotificationName:FlutterViewControllerShowHomeIndicator
246 [[NSNotificationCenter defaultCenter]
247 postNotificationName:FlutterViewControllerHideHomeIndicator
250 if (
self.enableViewControllerBasedStatusBarAppearance) {
251 [_engine.get() viewController].prefersStatusBarHidden = statusBarShouldBeHidden;
263 - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
264 BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
265 if (
self.enableViewControllerBasedStatusBarAppearance) {
266 [_engine.get() viewController].prefersStatusBarHidden = !edgeToEdge;
276 [[NSNotificationCenter defaultCenter]
277 postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator
278 : FlutterViewControllerHideHomeIndicator
282 - (void)restoreSystemChromeSystemUIOverlays {
286 - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
287 NSString* brightness = message[@"statusBarBrightness"];
288 if (brightness == (
id)[NSNull
null]) {
292 UIStatusBarStyle statusBarStyle;
293 if ([brightness isEqualToString:
@"Brightness.dark"]) {
294 statusBarStyle = UIStatusBarStyleLightContent;
295 }
else if ([brightness isEqualToString:
@"Brightness.light"]) {
296 if (@available(iOS 13, *)) {
297 statusBarStyle = UIStatusBarStyleDarkContent;
299 statusBarStyle = UIStatusBarStyleDefault;
305 if (
self.enableViewControllerBasedStatusBarAppearance) {
307 [[NSNotificationCenter defaultCenter]
308 postNotificationName:@(kOverlayStyleUpdateNotificationName)
310 userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
316 - (void)popSystemNavigator:(BOOL)isAnimated {
324 UINavigationController* navigationController = [engineViewController navigationController];
325 if (navigationController) {
326 [navigationController popViewControllerAnimated:isAnimated];
328 UIViewController* rootViewController = nil;
329 #if APPLICATION_EXTENSION_API_ONLY
330 if (@available(iOS 15.0, *)) {
332 [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController;
335 <<
"rootViewController is not available in application extension prior to iOS 15.0.";
338 rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
340 if (engineViewController != rootViewController) {
341 [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
346 - (NSDictionary*)getClipboardData:(NSString*)format {
347 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
348 if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
349 NSString* stringInPasteboard = pasteboard.string;
351 return stringInPasteboard == nil ? nil : @{
@"text" : stringInPasteboard};
356 - (void)setClipboardData:(NSDictionary*)data {
357 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
358 id copyText = data[@"text"];
359 if ([copyText isKindOfClass:[NSString
class]]) {
360 pasteboard.string = copyText;
362 pasteboard.string =
@"null";
366 - (NSDictionary*)clipboardHasStrings {
367 return @{
@"value" : @([UIPasteboard generalPasteboard].hasStrings)};
370 - (BOOL)isLiveTextInputAvailable {
371 return [[
self textField] canPerformAction:@selector(captureTextFromCamera:) withSender:nil];
374 - (void)showLookUpViewController:(NSString*)term {
375 UIViewController* engineViewController = [_engine.get() viewController];
376 UIReferenceLibraryViewController* referenceLibraryViewController =
377 [[[UIReferenceLibraryViewController alloc] initWithTerm:term] autorelease];
378 [engineViewController presentViewController:referenceLibraryViewController
383 - (UITextField*)textField {
391 [_textField release];