diff --git a/sky/services/platform/ios/system_chrome_impl.h b/sky/services/platform/ios/system_chrome_impl.h index 50755ea56d30d..8a0ef6d3d0642 100644 --- a/sky/services/platform/ios/system_chrome_impl.h +++ b/sky/services/platform/ios/system_chrome_impl.h @@ -22,10 +22,18 @@ class SystemChromeImpl : public SystemChrome { uint32_t device_orientation_mask, const SetPreferredOrientationsCallback& callback) override; + void SetApplicationSwitcherDescription( + ApplicationSwitcherDescriptionPtr description, + const SetApplicationSwitcherDescriptionCallback& callback) override; + void SetEnabledSystemUIOverlays( uint32_t overlays, const SetEnabledSystemUIOverlaysCallback& callback) override; + void SetSystemUIOverlayStyle( + SystemUIOverlayStyle style, + const SetSystemUIOverlayStyleCallback& callback) override; + private: mojo::StrongBinding binding_; @@ -34,6 +42,8 @@ class SystemChromeImpl : public SystemChrome { extern const char* const kOrientationUpdateNotificationName; extern const char* const kOrientationUpdateNotificationKey; +extern const char* const kOverlayStyleUpdateNotificationName; +extern const char* const kOverlayStyleUpdateNotificationKey; } // namespace platform } // namespace flutter diff --git a/sky/services/platform/ios/system_chrome_impl.mm b/sky/services/platform/ios/system_chrome_impl.mm index cadd139c4ec5f..4082bbc287879 100644 --- a/sky/services/platform/ios/system_chrome_impl.mm +++ b/sky/services/platform/ios/system_chrome_impl.mm @@ -4,6 +4,7 @@ #include "sky/services/platform/ios/system_chrome_impl.h" #include "base/mac/scoped_nsautorelease_pool.h" +#include #include namespace flutter { @@ -59,8 +60,15 @@ static constexpr bool IsSet(uint32_t mask, T orientation) { callback.Run(true); } +void SystemChromeImpl::SetApplicationSwitcherDescription( + ApplicationSwitcherDescriptionPtr description, + const SetApplicationSwitcherDescriptionCallback& callback) { + // No counterpart on iOS but is a benign operation. So no asserts. + callback.Run(true); +} + void SystemChromeImpl::SetEnabledSystemUIOverlays( - uint32_t overlays, + uint32_t overlay_mask, const SetEnabledSystemUIOverlaysCallback& callback) { // Checks if the top status bar should be visible. This platform ignores all // other overlays @@ -71,15 +79,56 @@ static constexpr bool IsSet(uint32_t mask, T orientation) { // to be able to modify this on the fly. The key used is // UIViewControllerBasedStatusBarAppearance [UIApplication sharedApplication].statusBarHidden = - !IsSet(overlays, SystemUIOverlay::Top); + !IsSet(overlay_mask, SystemUIOverlay::Top); + + callback.Run(true); +} + +void SystemChromeImpl::SetSystemUIOverlayStyle( + SystemUIOverlayStyle style, + const SetSystemUIOverlayStyleCallback& callback) { + base::mac::ScopedNSAutoreleasePool pool; + + UIStatusBarStyle statusBarStyle; + switch (style) { + case SystemUIOverlayStyle::Light: + statusBarStyle = UIStatusBarStyleLightContent; + break; + case SystemUIOverlayStyle::Dark: + statusBarStyle = UIStatusBarStyleDefault; + break; + } + + NSNumber* infoValue = [[NSBundle mainBundle] + objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]; + Boolean delegateToViewController = + (infoValue == nil || [infoValue boolValue]); + + if (delegateToViewController) { + // This notification is respected by the iOS embedder + [[NSNotificationCenter defaultCenter] + postNotificationName:@(kOverlayStyleUpdateNotificationName) + object:nil + userInfo:@{ + @(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle) + }]; + } else { + // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 + // in favor of delegating to the view controller + [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle]; + } callback.Run(true); } const char* const kOrientationUpdateNotificationName = - "SystemChromeOrientationNotificationName"; + "io.flutter.SystemChromeOrientationNotificationName"; const char* const kOrientationUpdateNotificationKey = - "SystemChromeOrientationNotificationName"; + "io.flutter.SystemChromeOrientationNotificationKey"; +const char* const kOverlayStyleUpdateNotificationName = + "io.flutter.SystemChromeOverlayNotificationName"; +const char* const kOverlayStyleUpdateNotificationKey = + "io.flutter.SystemChromeOverlayNotificationKey"; } // namespace platform } // namespace flutter diff --git a/sky/services/platform/src/org/domokit/platform/SystemChromeImpl.java b/sky/services/platform/src/org/domokit/platform/SystemChromeImpl.java index ea644ff79d1c9..53909c2fc7329 100644 --- a/sky/services/platform/src/org/domokit/platform/SystemChromeImpl.java +++ b/sky/services/platform/src/org/domokit/platform/SystemChromeImpl.java @@ -6,9 +6,11 @@ import android.app.Activity; import android.content.pm.ActivityInfo; +import android.os.Build; import android.view.View; import org.chromium.mojo.system.MojoException; +import org.chromium.mojom.flutter.platform.ApplicationSwitcherDescription; import org.chromium.mojom.flutter.platform.DeviceOrientation; import org.chromium.mojom.flutter.platform.SystemChrome; import org.chromium.mojom.flutter.platform.SystemUiOverlay; @@ -54,6 +56,31 @@ public void setPreferredOrientations(int deviceOrientationMask, callback.call(true); } + @Override + public void setApplicationSwitcherDescription( + ApplicationSwitcherDescription description, + SetApplicationSwitcherDescriptionResponse callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + callback.call(true); + return; + } + + int color = description.primaryColor; + if (color != 0) { // 0 means color isn't set, use system default + color = color | 0xFF000000; // color must be opaque if set + } + + mActivity.setTaskDescription( + new android.app.ActivityManager.TaskDescription( + description.label, + null, + color + ) + ); + + callback.call(true); + } + @Override public void setEnabledSystemUiOverlays(int overlays, SetEnabledSystemUiOverlaysResponse callback) { @@ -71,4 +98,12 @@ public void setEnabledSystemUiOverlays(int overlays, mActivity.getWindow().getDecorView().setSystemUiVisibility(flags); callback.call(true); } + + @Override + public void setSystemUiOverlayStyle(int style, SetSystemUiOverlayStyleResponse callback) { + // You can change the navigation bar color (including translucent colors) + // in Android, but you can't change the color of the navigation buttons, + // so LIGHT vs DARK effectively isn't supported in Android. + callback.call(true); + } } diff --git a/sky/services/platform/system_chrome.mojom b/sky/services/platform/system_chrome.mojom index b852f50a77f58..036181320d442 100644 --- a/sky/services/platform/system_chrome.mojom +++ b/sky/services/platform/system_chrome.mojom @@ -39,29 +39,66 @@ enum SystemUIOverlay { Bottom = 2, }; +/// Specifies a preference for the style of the system overlays. Certain +/// platforms may not respect this preference. +enum SystemUIOverlayStyle { + /// System overlays should be drawn with a light color. Intended for + /// applications with a dark background. + Light = 1, + + /// System overlays should be drawn with a dark color. Intended for + /// applications with a light background. + Dark = 2, +}; + +/// Specifies a description of the application that is pertinent to the +/// embedder's application switcher (a.k.a. "recent tasks") user interface. +struct ApplicationSwitcherDescription { + /// A label and description of the current state of the application. + string? label; + + /// The application's primary color. + uint32 primaryColor; +}; + /// Controls specific aspects of the embedder interface. [ServiceName="flutter::platform::SystemChrome"] interface SystemChrome { /// Specifies the set of orientations the application interface can /// be displayed in. /// - /// The value 0 is synonymous with having all options enabled. /// Arguments: - /// device_orientation_mask: A mask of `DeviceOrientation` enum values. + /// device_orientation_mask: A mask of `DeviceOrientation` enum values. + /// A value of 0 is synonymous with having all options enabled. /// /// Return Value: /// boolean indicating if the orientation mask is valid and the changes /// could be conveyed successfully to the embedder. SetPreferredOrientations(uint32 device_orientation_mask) => (bool success); + /// Specifies the description of the application within the embedder's + /// application switcher (a.k.a. "recent tasks") user interface. + /// + /// Arguments: + /// description: The description of the current state of the application. + /// + /// Return value: + /// boolean indicating if the preference was conveyed successfully to the + /// embedder. + /// + /// Platform Specific Notes: + /// If application switcher metadata cannot be manually set on the platform, + /// specifying such metadata is a no-op and always return true. + SetApplicationSwitcherDescription(ApplicationSwitcherDescription description) => (bool success); /// Specifies the set of overlays visible on the embedder when the /// application is running. The embedder may choose to ignore unsupported /// overlays /// /// Arguments: - /// style: A mask of `SystemUIOverlay` enum values that denotes the overlays - /// to show. + /// overlay_mask: A mask of `SystemUIOverlay` enum values that denotes the + /// overlays to show. A value of 0 is synonymous with showing no + /// overlays. /// /// Return Value: /// boolean indicating if the preference was conveyed successfully to the @@ -70,5 +107,20 @@ interface SystemChrome { /// Platform Specific Notes: /// If the overlay is unsupported on the platform, enabling or disabling /// that overlay is a no-op and always return true. - SetEnabledSystemUIOverlays(uint32 overlays) => (bool success); + SetEnabledSystemUIOverlays(uint32 overlay_mask) => (bool success); + + /// Specifies the style of the overlays that are visible on the embedder when + /// the applicatiomn is running. + /// + /// Arguments: + /// style: A `SystemUIOverlayStyle` enum value that denotes the style + /// + /// Return value: + /// boolean indicating if the preference was conveyed successfully to the + /// embedder. + /// + /// Platform Specific Notes: + /// If overlay style is unsupported on the platform, specifying a style is + /// a no-op and always return true. + SetSystemUIOverlayStyle(SystemUIOverlayStyle style) => (bool success); }; diff --git a/sky/shell/platform/ios/framework/Source/FlutterViewController.mm b/sky/shell/platform/ios/framework/Source/FlutterViewController.mm index da70ff76c1e69..544ecf3e63a60 100644 --- a/sky/shell/platform/ios/framework/Source/FlutterViewController.mm +++ b/sky/shell/platform/ios/framework/Source/FlutterViewController.mm @@ -43,6 +43,7 @@ void FlutterInit(int argc, const char* argv[]) { @implementation FlutterViewController { base::scoped_nsprotocol _dartProject; UIInterfaceOrientationMask _orientationPreferences; + UIStatusBarStyle _statusBarStyle; base::scoped_nsprotocol _dynamicServiceLoader; sky::ViewportMetricsPtr _viewportMetrics; sky::shell::TouchMapper _touchMapper; @@ -87,6 +88,7 @@ - (void)performCommonViewControllerInitialization { _initialized = YES; _orientationPreferences = UIInterfaceOrientationMaskAll; + _statusBarStyle = UIStatusBarStyleDefault; _dynamicServiceLoader.reset([[FlutterDynamicServiceLoader alloc] init]); _viewportMetrics = sky::ViewportMetrics::New(); _shellView = @@ -116,6 +118,11 @@ - (void)setupNotificationCenterObservers { name:@(flutter::platform::kOrientationUpdateNotificationName) object:nil]; + [center addObserver:self + selector:@selector(onPreferredStatusBarStyleUpdated:) + name:@(flutter::platform::kOverlayStyleUpdateNotificationName) + object:nil]; + [center addObserver:self selector:@selector(applicationBecameActive:) name:UIApplicationDidBecomeActiveNotification @@ -450,15 +457,38 @@ - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } -- (UIStatusBarStyle)preferredStatusBarStyle { - return UIStatusBarStyleLightContent; -} - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } +#pragma mark - Status bar style + +- (UIStatusBarStyle)preferredStatusBarStyle { + return _statusBarStyle; +} + +- (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification { + // Notifications may not be on the iOS UI thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary* info = notification.userInfo; + + NSNumber* update = + info[@(flutter::platform::kOverlayStyleUpdateNotificationKey)]; + + if (update == nil) { + return; + } + + NSInteger style = update.integerValue; + + if (style != _statusBarStyle) { + _statusBarStyle = static_cast(style); + [self setNeedsStatusBarAppearanceUpdate]; + } + }); +} + #pragma mark - Application Messages - (void)sendString:(NSString*)message withMessageName:(NSString*)messageName { @@ -497,14 +527,16 @@ - (void)removeMessageListener:(NSObject*)listener { _appMessageReceiver.SetMessageListener(messageName.UTF8String, nil); } -- (void)addAsyncMessageListener:(NSObject*)listener { +- (void)addAsyncMessageListener: + (NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null"); _appMessageReceiver.SetAsyncMessageListener(messageName.UTF8String, listener); } -- (void)removeAsyncMessageListener:(NSObject*)listener { +- (void)removeAsyncMessageListener: + (NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null");