diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index 06833cbe2c225..6b0a9e2ec0d70 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -14,6 +14,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.json.JSONArray; import org.json.JSONException; @@ -102,6 +103,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.error("error", exception.getMessage(), null); } break; + case "SystemChrome.setEnabledSystemUIMode": + try { + SystemUiMode mode = decodeSystemUiMode((String) arguments); + platformMessageHandler.showSystemUiMode(mode); + result.success(null); + } catch (JSONException | NoSuchFieldException exception) { + // JSONException: One or more expected fields were either omitted or referenced an + // invalid type. + // NoSuchFieldException: One or more of the overlay names are invalid. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemChrome.setSystemUIChangeListener": + platformMessageHandler.setSystemUiChangeListener(); + result.success(null); + break; case "SystemChrome.restoreSystemUIOverlays": platformMessageHandler.restoreSystemUiOverlays(); result.success(null); @@ -194,6 +211,12 @@ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformM this.platformMessageHandler = platformMessageHandler; } + /** Informs Flutter of a change in the SystemUI overlays. */ + public void systemChromeChanged(boolean overlaysAreVisible) { + Log.v(TAG, "Sending 'systemUIChange' message."); + channel.invokeMethod("SystemChrome.systemUIChange", Arrays.asList(overlaysAreVisible)); + } + // TODO(mattcarroll): add support for IntDef annotations, then add @ScreenOrientation /** @@ -313,6 +336,32 @@ private List decodeSystemUiOverlays(@NonNull JSONArray encodedS return overlays; } + /** + * Decodes an object of JSON-encoded mode to a {@link SystemUiMode}. + * + * @throws JSONException if {@code encodedSystemUiMode} does not contain expected keys and value + * types. + * @throws NoSuchFieldException if any of the given encoded mode name is invalid. + */ + @NonNull + private SystemUiMode decodeSystemUiMode(@NonNull String encodedSystemUiMode) + throws JSONException, NoSuchFieldException { + SystemUiMode mode = SystemUiMode.fromValue(encodedSystemUiMode); + switch (mode) { + case LEAN_BACK: + return SystemUiMode.LEAN_BACK; + case IMMERSIVE: + return SystemUiMode.IMMERSIVE; + case IMMERSIVE_STICKY: + return SystemUiMode.IMMERSIVE_STICKY; + case EDGE_TO_EDGE: + return SystemUiMode.EDGE_TO_EDGE; + } + + // Execution should never ever get this far, but if it does, we default to edge to edge. + return SystemUiMode.EDGE_TO_EDGE; + } + /** * Decodes a JSON-encoded {@code encodedStyle} to a {@link SystemChromeStyle}. * @@ -322,22 +371,19 @@ private List decodeSystemUiOverlays(@NonNull JSONArray encodedS @NonNull private SystemChromeStyle decodeSystemChromeStyle(@NonNull JSONObject encodedStyle) throws JSONException, NoSuchFieldException { - Brightness systemNavigationBarIconBrightness = null; + // TODO(mattcarroll): add color annotation + Integer statusBarColor = null; + Brightness statusBarIconBrightness = null; + boolean systemStatusBarContrastEnforced = true; // TODO(mattcarroll): add color annotation Integer systemNavigationBarColor = null; + Brightness systemNavigationBarIconBrightness = null; // TODO(mattcarroll): add color annotation Integer systemNavigationBarDividerColor = null; - Brightness statusBarIconBrightness = null; - // TODO(mattcarroll): add color annotation - Integer statusBarColor = null; + boolean systemNavigationBarContrastEnforced = true; - if (!encodedStyle.isNull("systemNavigationBarIconBrightness")) { - systemNavigationBarIconBrightness = - Brightness.fromValue(encodedStyle.getString("systemNavigationBarIconBrightness")); - } - - if (!encodedStyle.isNull("systemNavigationBarColor")) { - systemNavigationBarColor = encodedStyle.getInt("systemNavigationBarColor"); + if (!encodedStyle.isNull("statusBarColor")) { + statusBarColor = encodedStyle.getInt("statusBarColor"); } if (!encodedStyle.isNull("statusBarIconBrightness")) { @@ -345,20 +391,36 @@ private SystemChromeStyle decodeSystemChromeStyle(@NonNull JSONObject encodedSty Brightness.fromValue(encodedStyle.getString("statusBarIconBrightness")); } - if (!encodedStyle.isNull("statusBarColor")) { - statusBarColor = encodedStyle.getInt("statusBarColor"); + if (!encodedStyle.isNull("systemStatusBarContrastEnforced")) { + systemStatusBarContrastEnforced = encodedStyle.getBoolean("systemStatusBarContrastEnforced"); + } + + if (!encodedStyle.isNull("systemNavigationBarColor")) { + systemNavigationBarColor = encodedStyle.getInt("systemNavigationBarColor"); + } + + if (!encodedStyle.isNull("systemNavigationBarIconBrightness")) { + systemNavigationBarIconBrightness = + Brightness.fromValue(encodedStyle.getString("systemNavigationBarIconBrightness")); } if (!encodedStyle.isNull("systemNavigationBarDividerColor")) { systemNavigationBarDividerColor = encodedStyle.getInt("systemNavigationBarDividerColor"); } + if (!encodedStyle.isNull("systemNavigationBarContrastEnforced")) { + systemNavigationBarContrastEnforced = + encodedStyle.getBoolean("systemNavigationBarContrastEnforced"); + } + return new SystemChromeStyle( statusBarColor, statusBarIconBrightness, + systemStatusBarContrastEnforced, systemNavigationBarColor, systemNavigationBarIconBrightness, - systemNavigationBarDividerColor); + systemNavigationBarDividerColor, + systemNavigationBarContrastEnforced); } /** @@ -399,12 +461,43 @@ public interface PlatformMessageHandler { */ void showSystemOverlays(@NonNull List overlays); + /** + * The Flutter application would like the Android system to display the given {@code mode}. + * + *

{@link SystemUiMode#LEAN_BACK} refers to a fullscreen experience that restores system bars + * upon tapping anywhere in the application. This tap gesture is not received by the + * application. + * + *

{@link SystemUiMode#IMMERSIVE} refers to a fullscreen experience that restores system bars + * upon swiping from the edge of the viewport. This swipe gesture is not recived by the + * application. + * + *

{@link SystemUiMode#IMMERSIVE_STICKY} refers to a fullscreen experience that restores + * system bars upon swiping from the edge of the viewport. This swipe gesture is received by the + * application, in contrast to {@link SystemUiMode#IMMERSIVE}. + * + *

{@link SystemUiMode#EDGE_TO_EDGE} refers to a layout configuration that will consume the + * full viewport. This full screen experience does not hide status bars. These status bars can + * be set to transparent, making the buttons and icons hover over the fullscreen application. + */ + void showSystemUiMode(@NonNull SystemUiMode mode); + + /** + * The Flutter application would like the Android system to notify the framework when the system + * ui visibility has changed. + * + *

This is relevant when using {@link SystemUiMode}s for fullscreen applications, from which + * the system overlays can appear or disappear based on user input. + */ + void setSystemUiChangeListener(); + /** * The Flutter application would like to restore the visibility of system overlays to the last - * set of overlays sent via {@link #showSystemOverlays(List)}. + * set of overlays sent via {@link #showSystemOverlays(List)} or {@link + * #showSystemUiMode(SystemUiMode)}. * - *

If {@link #showSystemOverlays(List)} has yet to be called, then a default system overlay - * appearance is desired: + *

If {@link #showSystemOverlays(List)} or {@link #showSystemUiMode(SystemUiMode)} has yet to + * be called, then a default system overlay appearance is desired: * *

{@code View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN } */ @@ -542,6 +635,35 @@ static SystemUiOverlay fromValue(@NonNull String encodedName) throws NoSuchField } } + /** The set of Android system fullscreen modes as perceived by the Flutter application. */ + public enum SystemUiMode { + LEAN_BACK("SystemUiMode.leanBack"), + IMMERSIVE("SystemUiMode.immersive"), + IMMERSIVE_STICKY("SystemUiMode.immersiveSticky"), + EDGE_TO_EDGE("SystemUiMode.edgeToEdge"); + + /** + * Returns the SystemUiMode for the provied encoded value. @throws NoSuchFieldException if any + * of the given encoded overlay names are invalid. + */ + @NonNull + static SystemUiMode fromValue(@NonNull String encodedName) throws NoSuchFieldException { + for (SystemUiMode mode : SystemUiMode.values()) { + if (mode.encodedName.equals(encodedName)) { + return mode; + } + } + throw new NoSuchFieldException("No such SystemUiMode: " + encodedName); + } + + @NonNull private String encodedName; + + /** Returens the encoded {@link SystemUiMode} */ + SystemUiMode(@NonNull String encodedName) { + this.encodedName = encodedName; + } + } + /** * The color and label of an application that appears in Android's app switcher, AKA recents * screen. @@ -562,23 +684,29 @@ public static class SystemChromeStyle { // TODO(mattcarroll): add color annotation @Nullable public final Integer statusBarColor; @Nullable public final Brightness statusBarIconBrightness; + @Nullable public final boolean systemStatusBarContrastEnforced; // TODO(mattcarroll): add color annotation @Nullable public final Integer systemNavigationBarColor; @Nullable public final Brightness systemNavigationBarIconBrightness; // TODO(mattcarroll): add color annotation @Nullable public final Integer systemNavigationBarDividerColor; + @Nullable public final boolean systemNavigationBarContrastEnforced; public SystemChromeStyle( @Nullable Integer statusBarColor, @Nullable Brightness statusBarIconBrightness, + @Nullable boolean systemStatusBarContrastEnforced, @Nullable Integer systemNavigationBarColor, @Nullable Brightness systemNavigationBarIconBrightness, - @Nullable Integer systemNavigationBarDividerColor) { + @Nullable Integer systemNavigationBarDividerColor, + @Nullable boolean systemNavigationBarContrastEnforced) { this.statusBarColor = statusBarColor; this.statusBarIconBrightness = statusBarIconBrightness; + this.systemStatusBarContrastEnforced = systemStatusBarContrastEnforced; this.systemNavigationBarColor = systemNavigationBarColor; this.systemNavigationBarIconBrightness = systemNavigationBarIconBrightness; this.systemNavigationBarDividerColor = systemNavigationBarDividerColor; + this.systemNavigationBarContrastEnforced = systemNavigationBarContrastEnforced; } } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 03cc41ed7e3cc..cc3cb2a7ff0b8 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -85,6 +85,16 @@ public void showSystemOverlays(@NonNull List ov setSystemChromeEnabledSystemUIOverlays(overlays); } + @Override + public void showSystemUiMode(@NonNull PlatformChannel.SystemUiMode mode) { + setSystemChromeEnabledSystemUIMode(mode); + } + + @Override + public void setSystemUiChangeListener() { + setSystemChromeChangeListener(); + } + @Override public void restoreSystemUiOverlays() { restoreSystemChromeSystemUIOverlays(); @@ -204,6 +214,101 @@ private void setSystemChromeApplicationSwitcherDescription( } } + private void setSystemChromeChangeListener() { + // Set up a listener to notify the framework when the system ui has changed. + View decorView = activity.getWindow().getDecorView(); + decorView.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + // The system bars are visible. Make any desired adjustments to + // your UI, such as showing the action bar or other navigational + // controls. Another common action is to set a timer to dismiss + // the system bars and restore the fullscreen mode that was + // previously enabled. + platformChannel.systemChromeChanged(false); + } else { + // The system bars are NOT visible. Make any desired adjustments + // to your UI, such as hiding the action bar or other + // navigational controls. + platformChannel.systemChromeChanged(true); + } + } + }); + } + + private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode systemUiMode) { + int enabledOverlays = + DEFAULT_SYSTEM_UI + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + + if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // LEAN BACK + // Available starting at SDK 16 + // Should not show overlays, tap to reveal overlays, needs onChange callback + // When the overlays come in on tap, the app does not recieve the gesture and does not know + // the system overlay has changed. The overlays cannot be dismissed, so adding the callback + // support will allow users to restore the system ui and dismiss the overlays. + // Not compatible with top/bottom overlays enabled. + enabledOverlays = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN; + } else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // IMMERSIVE + // Available starting at 19 + // Should not show overlays, swipe from edges to reveal overlays, needs onChange callback + // When the overlays come in on swipe, the app does not receive the gesture and does not know + // the system overlay has changed. The overlays cannot be dismissed, so adding callback + // support will allow users to restore the system ui and dismiss the overlays. + // Not compatible with top/bottom overlays enabled. + enabledOverlays = + View.SYSTEM_UI_FLAG_IMMERSIVE + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN; + } else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE_STICKY + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // STICKY IMMERSIVE + // Available starting at 19 + // Should not show overlays, swipe from edges to reveal overlays. The app will also receive + // the swipe gesture. The overlays cannot be dismissed, so adding callback support will + // allow users to restore the system ui and dismiss the overlays. + // Not compatible with top/bottom overlays enabled. + enabledOverlays = + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN; + } else if (systemUiMode == PlatformChannel.SystemUiMode.EDGE_TO_EDGE + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // EDGE TO EDGE + // Available starting at 16 + // SDK 29 and up will apply a translucent body scrim behind 2/3 button navigation bars + // to ensure contrast with buttons on the nav bar. + // SDK 28 and lower will support a transparent 2/3 button navigation bar. + // Overlays should be included and not removed. + enabledOverlays = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + } + + mEnabledOverlays = enabledOverlays; + updateSystemUiOverlays(); + } + private void setSystemChromeEnabledSystemUIOverlays( List overlaysToShow) { // Start by assuming we want to hide all system overlays (like an immersive @@ -263,49 +368,75 @@ private void setSystemChromeSystemUIOverlayStyle( Window window = activity.getWindow(); View view = window.getDecorView(); int flags = view.getSystemUiVisibility(); - // You can change the navigation bar color (including translucent colors) - // in Android, but you can't change the color of the navigation buttons until - // Android O. - // LIGHT vs DARK effectively isn't supported until then. - // Build.VERSION_CODES.O - if (Build.VERSION.SDK_INT >= 26) { - if (systemChromeStyle.systemNavigationBarIconBrightness != null) { - switch (systemChromeStyle.systemNavigationBarIconBrightness) { - case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - flags |= 0x10; - break; - case LIGHT: - flags &= ~0x10; - break; - } + + // SYSTEM STATUS BAR ------------------------------------------------------------------- + // You can't change the color of the system status bar until SDK 21, and you can't change the + // color of the status icons until SDK 23. We only allow both starting at 23 to ensure buttons + // and icons can be visible when changing the background color. + // If transparent, SDK 29 and higher may apply a translucent scrim behind the bar to ensure + // proper contrast. This can be overridden with + // SystemChromeStyle.systemStatusBarContrastEnforced. + if (systemChromeStyle.statusBarIconBrightness != null && Build.VERSION.SDK_INT >= 23) { + switch (systemChromeStyle.statusBarIconBrightness) { + case DARK: + // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + flags |= 0x2000; + break; + case LIGHT: + flags &= ~0x2000; + break; } - if (systemChromeStyle.systemNavigationBarColor != null) { - window.setNavigationBarColor(systemChromeStyle.systemNavigationBarColor); + + if (systemChromeStyle.statusBarColor != null) { + window.setStatusBarColor(systemChromeStyle.statusBarColor); } } - // Build.VERSION_CODES.M - if (Build.VERSION.SDK_INT >= 23) { - if (systemChromeStyle.statusBarIconBrightness != null) { - switch (systemChromeStyle.statusBarIconBrightness) { - case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - flags |= 0x2000; - break; - case LIGHT: - flags &= ~0x2000; - break; - } + // You can't override the enforced contrast for a transparent status bar until SDK 29. + // This overrides the translucent scrim that may be placed behind the bar on SDK 29+ to ensure + // contrast is appropriate when using full screen layout modes like Edge to Edge. + if (!systemChromeStyle.systemStatusBarContrastEnforced && Build.VERSION.SDK_INT >= 29) { + window.setStatusBarContrastEnforced(systemChromeStyle.systemStatusBarContrastEnforced); + } + + // SYSTEM NAVIGATION BAR -------------------------------------------------------------- + // You can't change the color of the system navigation bar until SDK 21, and you can't change + // the color of the navigation buttons until SDK 26. We only allow both starting at 26 to + // ensure buttons can be visible when changing the background color. + // If transparent, SDK 29 and higher may apply a translucent scrim behind 2/3 button navigation + // bars to ensure proper contrast. This can be overridden with + // SystemChromeStyle.systemNavigationBarContrastEnforced. + if (systemChromeStyle.systemNavigationBarIconBrightness != null + && Build.VERSION.SDK_INT >= 26) { + switch (systemChromeStyle.systemNavigationBarIconBrightness) { + case DARK: + // View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + flags |= 0x10; + break; + case LIGHT: + flags &= ~0x10; + break; } - if (systemChromeStyle.statusBarColor != null) { - window.setStatusBarColor(systemChromeStyle.statusBarColor); + + if (systemChromeStyle.systemNavigationBarColor != null) { + window.setNavigationBarColor(systemChromeStyle.systemNavigationBarColor); } } + // You can't change the color of the navigation bar divider color until SDK 28. if (systemChromeStyle.systemNavigationBarDividerColor != null && Build.VERSION.SDK_INT >= 28) { window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); window.setNavigationBarDividerColor(systemChromeStyle.systemNavigationBarDividerColor); } + + // You can't override the enforced contrast for a transparent navigation bar until SDK 29. + // This overrides the translucent scrim that may be placed behind 2/3 button navigation bars on + // SDK 29+ to ensure contrast is appropriate when using full screen layout modes like + // Edge to Edge. + if (!systemChromeStyle.systemNavigationBarContrastEnforced && Build.VERSION.SDK_INT >= 29) { + window.setNavigationBarContrastEnforced( + systemChromeStyle.systemNavigationBarContrastEnforced); + } + view.setSystemUiVisibility(flags); currentTheme = systemChromeStyle; } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index b57d12bac6f81..bdb1b11e6ddf5 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -129,7 +129,8 @@ public void setNavigationBarDividerColor() { when(fakeActivity.getWindow()).thenReturn(fakeWindow); PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); - SystemChromeStyle style = new SystemChromeStyle(0XFF000000, null, 0XFFC70039, null, 0XFF006DB3); + SystemChromeStyle style = + new SystemChromeStyle(0XFF000000, null, true, 0XFFC70039, null, 0XFF006DB3, true); if (Build.VERSION.SDK_INT >= 28) { platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); @@ -140,6 +141,60 @@ public void setNavigationBarDividerColor() { } } + @Config(sdk = 29) + @Test + public void setSystemUiMode() { + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + if (Build.VERSION.SDK_INT >= 28) { + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.LEAN_BACK); + assertEquals( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN, + fakeActivity.getWindow().getDecorView().getSystemUiVisibility()); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.IMMERSIVE); + assertEquals( + View.SYSTEM_UI_FLAG_IMMERSIVE + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN, + fakeActivity.getWindow().getDecorView().getSystemUiVisibility()); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.IMMERSIVE_STICKY); + assertEquals( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN, + fakeActivity.getWindow().getDecorView().getSystemUiVisibility()); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.EDGE_TO_EDGE); + assertEquals( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + fakeActivity.getWindow().getDecorView().getSystemUiVisibility()); + } + } + @Test public void popSystemNavigatorFlutterActivity() { Activity mockActivity = mock(Activity.class); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index b4d7f4f00319c..999f81d5bc618 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -74,6 +74,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIOverlays"]) { [self setSystemChromeEnabledSystemUIOverlays:args]; result(nil); + } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIMode"]) { + [self setSystemChromeEnabledSystemUIMode:args]; + result(nil); } else if ([method isEqualToString:@"SystemChrome.restoreSystemUIOverlays"]) { [self restoreSystemChromeSystemUIOverlays]; result(nil); @@ -176,6 +179,26 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { } } +- (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode { + // Checks if the top status bar should be visible, reflected by edge to edge setting. This + // platform ignores all other system ui modes. + + // We opt out of view controller based status bar visibility since we want + // to be able to modify this on the fly. The key used is + // UIViewControllerBasedStatusBarAppearance + [UIApplication sharedApplication].statusBarHidden = + ![mode isEqualToString:@"SystemUiMode.edgeToEdge"]; + if ([mode isEqualToString:@"SystemUiMode.edgeToEdge"]) { + [[NSNotificationCenter defaultCenter] + postNotificationName:FlutterViewControllerShowHomeIndicator + object:nil]; + } else { + [[NSNotificationCenter defaultCenter] + postNotificationName:FlutterViewControllerHideHomeIndicator + object:nil]; + } +} + - (void)restoreSystemChromeSystemUIOverlays { // Nothing to do on iOS. }