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