Flutter Engine
FlutterPlatformPlugin.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
6 
7 #import <AudioToolbox/AudioToolbox.h>
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIApplication.h>
10 #import <UIKit/UIKit.h>
11 
12 #include "flutter/fml/logging.h"
13 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
14 
15 namespace {
16 
17 constexpr char kTextPlainFormat[] = "text/plain";
18 const UInt32 kKeyPressClickSoundId = 1306;
19 
20 } // namespaces
21 
22 namespace flutter {
23 
24 // TODO(abarth): Move these definitions from system_chrome_impl.cc to here.
26  "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
28  "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
30  "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
32  "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
33 
34 } // namespace flutter
35 
36 using namespace flutter;
37 
38 @implementation FlutterPlatformPlugin {
40 }
41 
42 - (instancetype)init {
43  @throw([NSException exceptionWithName:@"FlutterPlatformPlugin must initWithEngine"
44  reason:nil
45  userInfo:nil]);
46 }
47 
48 - (instancetype)initWithEngine:(fml::WeakPtr<FlutterEngine>)engine {
49  FML_DCHECK(engine) << "engine must be set";
50  self = [super init];
51 
52  if (self) {
53  _engine = engine;
54  }
55 
56  return self;
57 }
58 
59 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
60  NSString* method = call.method;
61  id args = call.arguments;
62  if ([method isEqualToString:@"SystemSound.play"]) {
63  [self playSystemSound:args];
64  result(nil);
65  } else if ([method isEqualToString:@"HapticFeedback.vibrate"]) {
66  [self vibrateHapticFeedback:args];
67  result(nil);
68  } else if ([method isEqualToString:@"SystemChrome.setPreferredOrientations"]) {
69  [self setSystemChromePreferredOrientations:args];
70  result(nil);
71  } else if ([method isEqualToString:@"SystemChrome.setApplicationSwitcherDescription"]) {
72  [self setSystemChromeApplicationSwitcherDescription:args];
73  result(nil);
74  } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIOverlays"]) {
75  [self setSystemChromeEnabledSystemUIOverlays:args];
76  result(nil);
77  } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIMode"]) {
78  [self setSystemChromeEnabledSystemUIMode:args];
79  result(nil);
80  } else if ([method isEqualToString:@"SystemChrome.restoreSystemUIOverlays"]) {
81  [self restoreSystemChromeSystemUIOverlays];
82  result(nil);
83  } else if ([method isEqualToString:@"SystemChrome.setSystemUIOverlayStyle"]) {
84  [self setSystemChromeSystemUIOverlayStyle:args];
85  result(nil);
86  } else if ([method isEqualToString:@"SystemNavigator.pop"]) {
87  NSNumber* isAnimated = args;
88  [self popSystemNavigator:isAnimated.boolValue];
89  result(nil);
90  } else if ([method isEqualToString:@"Clipboard.getData"]) {
91  result([self getClipboardData:args]);
92  } else if ([method isEqualToString:@"Clipboard.setData"]) {
93  [self setClipboardData:args];
94  result(nil);
95  } else if ([method isEqualToString:@"Clipboard.hasStrings"]) {
96  result([self clipboardHasStrings]);
97  } else {
99  }
100 }
101 
102 - (void)playSystemSound:(NSString*)soundType {
103  if ([soundType isEqualToString:@"SystemSoundType.click"]) {
104  // All feedback types are specific to Android and are treated as equal on
105  // iOS.
106  AudioServicesPlaySystemSound(kKeyPressClickSoundId);
107  }
108 }
109 
110 - (void)vibrateHapticFeedback:(NSString*)feedbackType {
111  if (!feedbackType) {
112  AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
113  return;
114  }
115 
116  if (@available(iOS 10, *)) {
117  if ([@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
118  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] autorelease]
119  impactOccurred];
120  } else if ([@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
121  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] autorelease]
122  impactOccurred];
123  } else if ([@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
124  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] autorelease]
125  impactOccurred];
126  } else if ([@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
127  [[[[UISelectionFeedbackGenerator alloc] init] autorelease] selectionChanged];
128  }
129  }
130 }
131 
132 - (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
133  UIInterfaceOrientationMask mask = 0;
134 
135  if (orientations.count == 0) {
136  mask |= UIInterfaceOrientationMaskAll;
137  } else {
138  for (NSString* orientation in orientations) {
139  if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"])
140  mask |= UIInterfaceOrientationMaskPortrait;
141  else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"])
142  mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
143  else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"])
144  mask |= UIInterfaceOrientationMaskLandscapeLeft;
145  else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"])
146  mask |= UIInterfaceOrientationMaskLandscapeRight;
147  }
148  }
149 
150  if (!mask)
151  return;
152  [[NSNotificationCenter defaultCenter]
153  postNotificationName:@(kOrientationUpdateNotificationName)
154  object:nil
155  userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
156 }
157 
158 - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
159  // No counterpart on iOS but is a benign operation. So no asserts.
160 }
161 
162 - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
163  // Checks if the top status bar should be visible. This platform ignores all
164  // other overlays
165 
166  // We opt out of view controller based status bar visibility since we want
167  // to be able to modify this on the fly. The key used is
168  // UIViewControllerBasedStatusBarAppearance
169  [UIApplication sharedApplication].statusBarHidden =
170  ![overlays containsObject:@"SystemUiOverlay.top"];
171  if ([overlays containsObject:@"SystemUiOverlay.bottom"]) {
172  [[NSNotificationCenter defaultCenter]
173  postNotificationName:FlutterViewControllerShowHomeIndicator
174  object:nil];
175  } else {
176  [[NSNotificationCenter defaultCenter]
177  postNotificationName:FlutterViewControllerHideHomeIndicator
178  object:nil];
179  }
180 }
181 
182 - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
183  // Checks if the top status bar should be visible, reflected by edge to edge setting. This
184  // platform ignores all other system ui modes.
185 
186  // We opt out of view controller based status bar visibility since we want
187  // to be able to modify this on the fly. The key used is
188  // UIViewControllerBasedStatusBarAppearance
189  [UIApplication sharedApplication].statusBarHidden =
190  ![mode isEqualToString:@"SystemUiMode.edgeToEdge"];
191  if ([mode isEqualToString:@"SystemUiMode.edgeToEdge"]) {
192  [[NSNotificationCenter defaultCenter]
193  postNotificationName:FlutterViewControllerShowHomeIndicator
194  object:nil];
195  } else {
196  [[NSNotificationCenter defaultCenter]
197  postNotificationName:FlutterViewControllerHideHomeIndicator
198  object:nil];
199  }
200 }
201 
202 - (void)restoreSystemChromeSystemUIOverlays {
203  // Nothing to do on iOS.
204 }
205 
206 - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
207  NSString* brightness = message[@"statusBarBrightness"];
208  if (brightness == (id)[NSNull null])
209  return;
210 
211  UIStatusBarStyle statusBarStyle;
212  if ([brightness isEqualToString:@"Brightness.dark"]) {
213  statusBarStyle = UIStatusBarStyleLightContent;
214  } else if ([brightness isEqualToString:@"Brightness.light"]) {
215  if (@available(iOS 13, *)) {
216  statusBarStyle = UIStatusBarStyleDarkContent;
217  } else {
218  statusBarStyle = UIStatusBarStyleDefault;
219  }
220  } else {
221  return;
222  }
223 
224  NSNumber* infoValue = [[NSBundle mainBundle]
225  objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
226  Boolean delegateToViewController = (infoValue == nil || [infoValue boolValue]);
227 
228  if (delegateToViewController) {
229  // This notification is respected by the iOS embedder
230  [[NSNotificationCenter defaultCenter]
231  postNotificationName:@(kOverlayStyleUpdateNotificationName)
232  object:nil
233  userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
234  } else {
235  // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9
236  // in favor of delegating to the view controller
237  [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle];
238  }
239 }
240 
241 - (void)popSystemNavigator:(BOOL)isAnimated {
242  // Apple's human user guidelines say not to terminate iOS applications. However, if the
243  // root view of the app is a navigation controller, it is instructed to back up a level
244  // in the navigation hierarchy.
245  // It's also possible in an Add2App scenario that the FlutterViewController was presented
246  // outside the context of a UINavigationController, and still wants to be popped.
247  UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
248  if ([viewController isKindOfClass:[UINavigationController class]]) {
249  [((UINavigationController*)viewController) popViewControllerAnimated:isAnimated];
250  } else {
251  auto engineViewController = static_cast<UIViewController*>([_engine.get() viewController]);
252  if (engineViewController != viewController) {
253  [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
254  }
255  }
256 }
257 
258 - (NSDictionary*)getClipboardData:(NSString*)format {
259  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
260  if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
261  NSString* stringInPasteboard = pasteboard.string;
262  // The pasteboard may contain an item but it may not be a string (an image for instance).
263  return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard};
264  }
265  return nil;
266 }
267 
268 - (void)setClipboardData:(NSDictionary*)data {
269  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
270  if (data[@"text"]) {
271  pasteboard.string = data[@"text"];
272  } else {
273  pasteboard.string = @"null";
274  }
275 }
276 
277 - (NSDictionary*)clipboardHasStrings {
278  bool hasStrings = false;
279  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
280  if (@available(iOS 10, *)) {
281  hasStrings = pasteboard.hasStrings;
282  } else {
283  NSString* stringInPasteboard = pasteboard.string;
284  hasStrings = stringInPasteboard != nil;
285  }
286  return @{@"value" : @(hasStrings)};
287 }
288 
289 @end
G_BEGIN_DECLS FlValue * args
#define FML_DCHECK(condition)
Definition: logging.h:86
FlutterViewController * viewController
const char *const kOrientationUpdateNotificationKey
GAsyncResult * result
const char *const kOverlayStyleUpdateNotificationKey
uint32_t uint32_t * format
const char *const kOrientationUpdateNotificationName
fml::scoped_nsobject< FlutterEngine > _engine
static constexpr char kTextPlainFormat[]
const char *const kOverlayStyleUpdateNotificationName
void(^ FlutterResult)(id _Nullable result)
int BOOL
Definition: windows_types.h:37
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented