Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
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/FlutterEngine_Internal.h"
14#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
15#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
16#import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h"
17
18namespace {
19
20constexpr char kTextPlainFormat[] = "text/plain";
21const UInt32 kKeyPressClickSoundId = 1306;
22
23#if not APPLICATION_EXTENSION_API_ONLY
24const NSString* searchURLPrefix = @"x-web-search://?";
25#endif
26
27} // namespace
28
29namespace flutter {
30
31// TODO(abarth): Move these definitions from system_chrome_impl.cc to here.
33 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
35 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
37 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
39 "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
40
41} // namespace flutter
42
43using namespace flutter;
44
46#if not APPLICATION_EXTENSION_API_ONLY
47 [UIApplication sharedApplication].statusBarHidden = hidden;
48#else
49 FML_LOG(WARNING) << "Application based status bar styling is not available in app extension.";
50#endif
51}
52
53static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) {
54#if not APPLICATION_EXTENSION_API_ONLY
55 // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9
56 // in favor of delegating to the view controller.
57 [[UIApplication sharedApplication] setStatusBarStyle:style];
58#else
59 FML_LOG(WARNING) << "Application based status bar styling is not available in app extension.";
60#endif
61}
62
63@interface FlutterPlatformPlugin ()
64
65/**
66 * @brief Whether the status bar appearance is based on the style preferred for this ViewController.
67 *
68 * The default value is YES.
69 * Explicitly add `UIViewControllerBasedStatusBarAppearance` as `false` in
70 * info.plist makes this value to be false.
71 */
72@property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
73
74@end
75
76@implementation FlutterPlatformPlugin {
78 // Used to detect whether this device has live text input ability or not.
79 UITextField* _textField;
80}
81
82- (instancetype)initWithEngine:(fml::WeakNSObject<FlutterEngine>)engine {
83 FML_DCHECK(engine) << "engine must be set";
84 self = [super init];
85
86 if (self) {
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 "
93 "be a Boolean type.";
94 }
95#endif
96 _enableViewControllerBasedStatusBarAppearance =
97 (infoValue == nil || [(NSNumber*)infoValue boolValue]);
98 }
99
100 return self;
101}
102
103- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
104 NSString* method = call.method;
105 id args = call.arguments;
106 if ([method isEqualToString:@"SystemSound.play"]) {
107 [self playSystemSound:args];
108 result(nil);
109 } else if ([method isEqualToString:@"HapticFeedback.vibrate"]) {
110 [self vibrateHapticFeedback:args];
111 result(nil);
112 } else if ([method isEqualToString:@"SystemChrome.setPreferredOrientations"]) {
113 [self setSystemChromePreferredOrientations:args];
114 result(nil);
115 } else if ([method isEqualToString:@"SystemChrome.setApplicationSwitcherDescription"]) {
116 [self setSystemChromeApplicationSwitcherDescription:args];
117 result(nil);
118 } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIOverlays"]) {
119 [self setSystemChromeEnabledSystemUIOverlays:args];
120 result(nil);
121 } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIMode"]) {
122 [self setSystemChromeEnabledSystemUIMode:args];
123 result(nil);
124 } else if ([method isEqualToString:@"SystemChrome.restoreSystemUIOverlays"]) {
125 [self restoreSystemChromeSystemUIOverlays];
126 result(nil);
127 } else if ([method isEqualToString:@"SystemChrome.setSystemUIOverlayStyle"]) {
128 [self setSystemChromeSystemUIOverlayStyle:args];
129 result(nil);
130 } else if ([method isEqualToString:@"SystemNavigator.pop"]) {
131 NSNumber* isAnimated = args;
132 [self popSystemNavigator:isAnimated.boolValue];
133 result(nil);
134 } else if ([method isEqualToString:@"Clipboard.getData"]) {
135 result([self getClipboardData:args]);
136 } else if ([method isEqualToString:@"Clipboard.setData"]) {
137 [self setClipboardData:args];
138 result(nil);
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];
145 result(nil);
146 } else if ([method isEqualToString:@"LookUp.invoke"]) {
147 [self showLookUpViewController:args];
148 result(nil);
149 } else if ([method isEqualToString:@"Share.invoke"]) {
150 [self showShareViewController:args];
151 result(nil);
152 } else if ([method isEqualToString:@"ContextMenu.showSystemContextMenu"]) {
153 [self showSystemContextMenu:args];
154 result(nil);
155 } else if ([method isEqualToString:@"ContextMenu.hideSystemContextMenu"]) {
156 [self hideSystemContextMenu];
157 result(nil);
158 } else {
160 }
161}
162
163- (void)showSystemContextMenu:(NSDictionary*)args {
164 if (@available(iOS 16.0, *)) {
165 FlutterTextInputPlugin* textInputPlugin = [_engine.get() textInputPlugin];
166 BOOL shownEditMenu = [textInputPlugin showEditMenu:args];
167 if (!shownEditMenu) {
168 FML_LOG(ERROR) << "Only text input supports system context menu for now. Ensure the system "
169 "context menu is shown with an active text input connection. See "
170 "https://github.com/flutter/flutter/issues/143033.";
171 }
172 }
173}
174
175- (void)hideSystemContextMenu {
176 if (@available(iOS 16.0, *)) {
177 FlutterTextInputPlugin* textInputPlugin = [_engine.get() textInputPlugin];
178 [textInputPlugin hideEditMenu];
179 }
180}
181
182- (void)showShareViewController:(NSString*)content {
183 UIViewController* engineViewController = [_engine.get() viewController];
184
185 NSArray* itemsToShare = @[ content ?: [NSNull null] ];
186 UIActivityViewController* activityViewController =
187 [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare
188 applicationActivities:nil] autorelease];
189
190 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
191 // On iPad, the share screen is presented in a popover view, and requires a
192 // sourceView and sourceRect
193 FlutterTextInputPlugin* _textInputPlugin = [_engine.get() textInputPlugin];
194 UITextRange* range = _textInputPlugin.textInputView.selectedTextRange;
195
196 // firstRectForRange cannot be used here as it's current implementation does
197 // not always return the full rect of the range.
198 CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
199 caretRectForPosition:(FlutterTextPosition*)range.start];
200 CGRect transformedFirstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
201 localRectFromFrameworkTransform:firstRect];
202 CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
203 caretRectForPosition:(FlutterTextPosition*)range.end];
204 CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
205 localRectFromFrameworkTransform:lastRect];
206
207 activityViewController.popoverPresentationController.sourceView = engineViewController.view;
208 // In case of RTL Language, get the minimum x coordinate
209 activityViewController.popoverPresentationController.sourceRect =
210 CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x),
211 transformedFirstRect.origin.y,
212 abs(transformedLastRect.origin.x - transformedFirstRect.origin.x),
213 transformedFirstRect.size.height);
214 }
215
216 [engineViewController presentViewController:activityViewController animated:YES completion:nil];
217}
218
219- (void)searchWeb:(NSString*)searchTerm {
220#if APPLICATION_EXTENSION_API_ONLY
221 FML_LOG(WARNING) << "SearchWeb.invoke is not availabe in app extension.";
222#else
223 NSString* escapedText = [searchTerm
224 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
225 URLHostAllowedCharacterSet]];
226 NSString* searchURL = [NSString stringWithFormat:@"%@%@", searchURLPrefix, escapedText];
227
228 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL]
229 options:@{}
230 completionHandler:nil];
231#endif
232}
233
234- (void)playSystemSound:(NSString*)soundType {
235 if ([soundType isEqualToString:@"SystemSoundType.click"]) {
236 // All feedback types are specific to Android and are treated as equal on
237 // iOS.
238 AudioServicesPlaySystemSound(kKeyPressClickSoundId);
239 }
240}
241
242- (void)vibrateHapticFeedback:(NSString*)feedbackType {
243 if (!feedbackType) {
244 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
245 return;
246 }
247
248 if ([@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
249 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] autorelease]
250 impactOccurred];
251 } else if ([@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
252 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] autorelease]
253 impactOccurred];
254 } else if ([@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
255 [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] autorelease]
256 impactOccurred];
257 } else if ([@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
258 [[[[UISelectionFeedbackGenerator alloc] init] autorelease] selectionChanged];
259 }
260}
261
262- (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
263 UIInterfaceOrientationMask mask = 0;
264
265 if (orientations.count == 0) {
266 mask |= UIInterfaceOrientationMaskAll;
267 } else {
268 for (NSString* orientation in orientations) {
269 if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"]) {
270 mask |= UIInterfaceOrientationMaskPortrait;
271 } else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"]) {
272 mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
273 } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"]) {
274 mask |= UIInterfaceOrientationMaskLandscapeLeft;
275 } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"]) {
276 mask |= UIInterfaceOrientationMaskLandscapeRight;
277 }
278 }
279 }
280
281 if (!mask) {
282 return;
283 }
284 [[NSNotificationCenter defaultCenter]
285 postNotificationName:@(kOrientationUpdateNotificationName)
286 object:nil
287 userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
288}
289
290- (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
291 // No counterpart on iOS but is a benign operation. So no asserts.
292}
293
294- (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
295 BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"];
296 if ([overlays containsObject:@"SystemUiOverlay.bottom"]) {
297 [[NSNotificationCenter defaultCenter]
298 postNotificationName:FlutterViewControllerShowHomeIndicator
299 object:nil];
300 } else {
301 [[NSNotificationCenter defaultCenter]
302 postNotificationName:FlutterViewControllerHideHomeIndicator
303 object:nil];
304 }
305 if (self.enableViewControllerBasedStatusBarAppearance) {
306 [_engine.get() viewController].prefersStatusBarHidden = statusBarShouldBeHidden;
307 } else {
308 // Checks if the top status bar should be visible. This platform ignores all
309 // other overlays
310
311 // We opt out of view controller based status bar visibility since we want
312 // to be able to modify this on the fly. The key used is
313 // UIViewControllerBasedStatusBarAppearance.
314 SetStatusBarHiddenForSharedApplication(statusBarShouldBeHidden);
315 }
316}
317
318- (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
319 BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
320 if (self.enableViewControllerBasedStatusBarAppearance) {
321 [_engine.get() viewController].prefersStatusBarHidden = !edgeToEdge;
322 } else {
323 // Checks if the top status bar should be visible, reflected by edge to edge setting. This
324 // platform ignores all other system ui modes.
325
326 // We opt out of view controller based status bar visibility since we want
327 // to be able to modify this on the fly. The key used is
328 // UIViewControllerBasedStatusBarAppearance.
330 }
331 [[NSNotificationCenter defaultCenter]
332 postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator
333 : FlutterViewControllerHideHomeIndicator
334 object:nil];
335}
336
337- (void)restoreSystemChromeSystemUIOverlays {
338 // Nothing to do on iOS.
339}
340
341- (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
342 NSString* brightness = message[@"statusBarBrightness"];
343 if (brightness == (id)[NSNull null]) {
344 return;
345 }
346
347 UIStatusBarStyle statusBarStyle;
348 if ([brightness isEqualToString:@"Brightness.dark"]) {
349 statusBarStyle = UIStatusBarStyleLightContent;
350 } else if ([brightness isEqualToString:@"Brightness.light"]) {
351 if (@available(iOS 13, *)) {
352 statusBarStyle = UIStatusBarStyleDarkContent;
353 } else {
354 statusBarStyle = UIStatusBarStyleDefault;
355 }
356 } else {
357 return;
358 }
359
360 if (self.enableViewControllerBasedStatusBarAppearance) {
361 // This notification is respected by the iOS embedder.
362 [[NSNotificationCenter defaultCenter]
363 postNotificationName:@(kOverlayStyleUpdateNotificationName)
364 object:nil
365 userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
366 } else {
368 }
369}
370
371- (void)popSystemNavigator:(BOOL)isAnimated {
372 // Apple's human user guidelines say not to terminate iOS applications. However, if the
373 // root view of the app is a navigation controller, it is instructed to back up a level
374 // in the navigation hierarchy.
375 // It's also possible in an Add2App scenario that the FlutterViewController was presented
376 // outside the context of a UINavigationController, and still wants to be popped.
377
378 FlutterViewController* engineViewController = [_engine.get() viewController];
379 UINavigationController* navigationController = [engineViewController navigationController];
380 if (navigationController) {
381 [navigationController popViewControllerAnimated:isAnimated];
382 } else {
383 UIViewController* rootViewController = nil;
384#if APPLICATION_EXTENSION_API_ONLY
385 if (@available(iOS 15.0, *)) {
386 rootViewController =
387 [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController;
388 } else {
389 FML_LOG(WARNING)
390 << "rootViewController is not available in application extension prior to iOS 15.0.";
391 }
392#else
393 rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
394#endif
395 if (engineViewController != rootViewController) {
396 [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
397 }
398 }
399}
400
401- (NSDictionary*)getClipboardData:(NSString*)format {
402 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
403 if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
404 NSString* stringInPasteboard = pasteboard.string;
405 // The pasteboard may contain an item but it may not be a string (an image for instance).
406 return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard};
407 }
408 return nil;
409}
410
411- (void)setClipboardData:(NSDictionary*)data {
412 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
413 id copyText = data[@"text"];
414 if ([copyText isKindOfClass:[NSString class]]) {
415 pasteboard.string = copyText;
416 } else {
417 pasteboard.string = @"null";
418 }
419}
420
421- (NSDictionary*)clipboardHasStrings {
422 return @{@"value" : @([UIPasteboard generalPasteboard].hasStrings)};
423}
424
425- (BOOL)isLiveTextInputAvailable {
426 return [[self textField] canPerformAction:@selector(captureTextFromCamera:) withSender:nil];
427}
428
429- (void)showLookUpViewController:(NSString*)term {
430 UIViewController* engineViewController = [_engine.get() viewController];
431 UIReferenceLibraryViewController* referenceLibraryViewController =
432 [[[UIReferenceLibraryViewController alloc] initWithTerm:term] autorelease];
433 [engineViewController presentViewController:referenceLibraryViewController
434 animated:YES
435 completion:nil];
436}
437
438- (UITextField*)textField {
439 if (_textField == nil) {
440 _textField = [[UITextField alloc] init];
441 }
442 return _textField;
443}
444
445- (void)dealloc {
446 [_textField release];
447 [super dealloc];
448}
449@end
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style)
UITextField * _textField
static void SetStatusBarHiddenForSharedApplication(BOOL hidden)
FlutterEngine engine
Definition main.cc:68
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
GAsyncResult * result
#define FML_LOG(severity)
Definition logging.h:82
#define FML_DCHECK(condition)
Definition logging.h:103
UIView< UITextInput > * textInputView()
BOOL showEditMenu:(ios(16.0) API_AVAILABLE)
fml::scoped_nsobject< FlutterTextInputPlugin > _textInputPlugin
FlutterTextInputPlugin * textInputPlugin
fml::scoped_nsobject< FlutterEngine > _engine
Win32Message message
constexpr char kTextPlainFormat[]
Clipboard plain text format.
call(args)
Definition dom.py:159
const char *const kOrientationUpdateNotificationKey
const char *const kOverlayStyleUpdateNotificationName
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot data
Definition switches.h:41
const char *const kOverlayStyleUpdateNotificationKey
const char *const kOrientationUpdateNotificationName
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace Enable an endless trace buffer The default is a ring buffer This is useful when very old events need to viewed For during application launch Memory usage will continue to grow indefinitely however Start app with an specific route defined on the framework flutter assets Path to the Flutter assets directory enable service port Allow the VM service to fallback to automatic port selection if binding to a specified port fails trace Trace early application lifecycle Automatically switches to an endless trace buffer trace skia Filters out all Skia trace event categories except those that are specified in this comma separated list dump skp on shader Automatically dump the skp that triggers new shader compilations This is useful for writing custom ShaderWarmUp to reduce jank By this is not enabled to reduce the overhead purge persistent Remove all existing persistent cache This is mainly for debugging purposes such as reproducing the shader compilation jank trace to Write the timeline trace to a file at the specified path The file will be in Perfetto s proto format
Definition switches.h:203
SIN Vec< N, float > abs(const Vec< N, float > &x)
Definition SkVx.h:707
#define ERROR(message)
int BOOL