• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
6#include "flutter/fml/logging.h"
7
8#include <AudioToolbox/AudioToolbox.h>
9#include <Foundation/Foundation.h>
10#include <UIKit/UIApplication.h>
11#include <UIKit/UIKit.h>
12
13namespace {
14
15constexpr char kTextPlainFormat[] = "text/plain";
16const UInt32 kKeyPressClickSoundId = 1306;
17
18}  // namespaces
19
20namespace flutter {
21
22// TODO(abarth): Move these definitions from system_chrome_impl.cc to here.
23const char* const kOrientationUpdateNotificationName =
24    "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
25const char* const kOrientationUpdateNotificationKey =
26    "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
27const char* const kOverlayStyleUpdateNotificationName =
28    "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
29const char* const kOverlayStyleUpdateNotificationKey =
30    "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
31
32}  // namespace flutter
33
34using namespace flutter;
35
36@implementation FlutterPlatformPlugin {
37  fml::WeakPtr<FlutterEngine> _engine;
38}
39
40- (instancetype)init {
41  @throw([NSException exceptionWithName:@"FlutterPlatformPlugin must initWithEngine"
42                                 reason:nil
43                               userInfo:nil]);
44}
45
46- (instancetype)initWithEngine:(fml::WeakPtr<FlutterEngine>)engine {
47  FML_DCHECK(engine) << "engine must be set";
48  self = [super init];
49
50  if (self) {
51    _engine = engine;
52  }
53
54  return self;
55}
56
57- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
58  NSString* method = call.method;
59  id args = call.arguments;
60  if ([method isEqualToString:@"SystemSound.play"]) {
61    [self playSystemSound:args];
62    result(nil);
63  } else if ([method isEqualToString:@"HapticFeedback.vibrate"]) {
64    [self vibrateHapticFeedback:args];
65    result(nil);
66  } else if ([method isEqualToString:@"SystemChrome.setPreferredOrientations"]) {
67    [self setSystemChromePreferredOrientations:args];
68    result(nil);
69  } else if ([method isEqualToString:@"SystemChrome.setApplicationSwitcherDescription"]) {
70    [self setSystemChromeApplicationSwitcherDescription:args];
71    result(nil);
72  } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIOverlays"]) {
73    [self setSystemChromeEnabledSystemUIOverlays:args];
74    result(nil);
75  } else if ([method isEqualToString:@"SystemChrome.restoreSystemUIOverlays"]) {
76    [self restoreSystemChromeSystemUIOverlays];
77    result(nil);
78  } else if ([method isEqualToString:@"SystemChrome.setSystemUIOverlayStyle"]) {
79    [self setSystemChromeSystemUIOverlayStyle:args];
80    result(nil);
81  } else if ([method isEqualToString:@"SystemNavigator.pop"]) {
82    [self popSystemNavigator];
83    result(nil);
84  } else if ([method isEqualToString:@"Clipboard.getData"]) {
85    result([self getClipboardData:args]);
86  } else if ([method isEqualToString:@"Clipboard.setData"]) {
87    [self setClipboardData:args];
88    result(nil);
89  } else {
90    result(nil);
91  }
92}
93
94- (void)playSystemSound:(NSString*)soundType {
95  if ([soundType isEqualToString:@"SystemSoundType.click"]) {
96    // All feedback types are specific to Android and are treated as equal on
97    // iOS.
98    AudioServicesPlaySystemSound(kKeyPressClickSoundId);
99  }
100}
101
102- (void)vibrateHapticFeedback:(NSString*)feedbackType {
103  if (!feedbackType) {
104    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
105    return;
106  }
107
108  if (@available(iOS 10, *)) {
109    if ([@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
110      [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] impactOccurred];
111    } else if ([@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
112      [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]
113          impactOccurred];
114    } else if ([@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
115      [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] impactOccurred];
116    } else if ([@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
117      [[[UISelectionFeedbackGenerator alloc] init] selectionChanged];
118    }
119  }
120}
121
122- (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
123  UIInterfaceOrientationMask mask = 0;
124
125  if (orientations.count == 0) {
126    mask |= UIInterfaceOrientationMaskAll;
127  } else {
128    for (NSString* orientation in orientations) {
129      if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"])
130        mask |= UIInterfaceOrientationMaskPortrait;
131      else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"])
132        mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
133      else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"])
134        mask |= UIInterfaceOrientationMaskLandscapeLeft;
135      else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"])
136        mask |= UIInterfaceOrientationMaskLandscapeRight;
137    }
138  }
139
140  if (!mask)
141    return;
142  [[NSNotificationCenter defaultCenter]
143      postNotificationName:@(kOrientationUpdateNotificationName)
144                    object:nil
145                  userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
146}
147
148- (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
149  // No counterpart on iOS but is a benign operation. So no asserts.
150}
151
152- (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
153  // Checks if the top status bar should be visible. This platform ignores all
154  // other overlays
155
156  // We opt out of view controller based status bar visibility since we want
157  // to be able to modify this on the fly. The key used is
158  // UIViewControllerBasedStatusBarAppearance
159  [UIApplication sharedApplication].statusBarHidden =
160      ![overlays containsObject:@"SystemUiOverlay.top"];
161}
162
163- (void)restoreSystemChromeSystemUIOverlays {
164  // Nothing to do on iOS.
165}
166
167- (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
168  NSString* style = message[@"statusBarBrightness"];
169  if (style == (id)[NSNull null])
170    return;
171
172  UIStatusBarStyle statusBarStyle;
173  if ([style isEqualToString:@"Brightness.dark"])
174    statusBarStyle = UIStatusBarStyleLightContent;
175  else if ([style isEqualToString:@"Brightness.light"])
176    statusBarStyle = UIStatusBarStyleDefault;
177  else
178    return;
179
180  NSNumber* infoValue = [[NSBundle mainBundle]
181      objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
182  Boolean delegateToViewController = (infoValue == nil || [infoValue boolValue]);
183
184  if (delegateToViewController) {
185    // This notification is respected by the iOS embedder
186    [[NSNotificationCenter defaultCenter]
187        postNotificationName:@(kOverlayStyleUpdateNotificationName)
188                      object:nil
189                    userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
190  } else {
191    // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9
192    // in favor of delegating to the view controller
193    [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle];
194  }
195}
196
197- (void)popSystemNavigator {
198  // Apple's human user guidelines say not to terminate iOS applications. However, if the
199  // root view of the app is a navigation controller, it is instructed to back up a level
200  // in the navigation hierarchy.
201  // It's also possible in an Add2App scenario that the FlutterViewController was presented
202  // outside the context of a UINavigationController, and still wants to be popped.
203  UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
204  if ([viewController isKindOfClass:[UINavigationController class]]) {
205    [((UINavigationController*)viewController) popViewControllerAnimated:NO];
206  } else {
207    auto engineViewController = static_cast<UIViewController*>([_engine.get() viewController]);
208    if (engineViewController != viewController) {
209      [engineViewController dismissViewControllerAnimated:NO completion:nil];
210    }
211  }
212}
213
214- (NSDictionary*)getClipboardData:(NSString*)format {
215  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
216  if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
217    NSString* stringInPasteboard = pasteboard.string;
218    // The pasteboard may contain an item but it may not be a string (an image for instance).
219    return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard};
220  }
221  return nil;
222}
223
224- (void)setClipboardData:(NSDictionary*)data {
225  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
226  pasteboard.string = data[@"text"];
227}
228
229@end
230