diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java index 0fe692c49c6c7..d0bd885197dda 100644 --- a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -24,9 +24,9 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager.LayoutParams; +import androidx.core.view.WindowCompat; import io.flutter.Log; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.util.Preconditions; import io.flutter.view.FlutterMain; import io.flutter.view.FlutterNativeView; @@ -141,7 +141,14 @@ public void onCreate(Bundle savedInstanceState) { Window window = activity.getWindow(); window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(0x40000000); - window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < 30) { + // This ensures that the navigation bar is not hidden for APIs < 30, + // as dictated by the implementation of WindowCompat. + View view = window.getDecorView(); + view.setSystemUiVisibility( + view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } } String[] args = getArgsFromIntent(activity.getIntent()); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 2b915cc04ce3b..36567d90904f8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -38,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.content.res.ResourcesCompat; +import androidx.core.view.WindowCompat; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; @@ -590,7 +591,14 @@ private void configureStatusBarForFullscreenFlutterExperience() { Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(0x40000000); - window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < 30) { + // This ensures that the navigation bar is not hidden for APIs < 30, + // as dictated by the implementation of WindowCompat. + View view = window.getDecorView(); + view.setSystemUiVisibility( + view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index d7c8515b4869d..50382659ed370 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -38,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.content.res.ResourcesCompat; +import androidx.core.view.WindowCompat; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import io.flutter.Log; @@ -45,7 +46,6 @@ import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister; -import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.util.ViewUtils; /** @@ -495,7 +495,14 @@ private void configureStatusBarForFullscreenFlutterExperience() { Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(0x40000000); - window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < 30) { + // This ensures that the navigation bar is not hidden for APIs < 30, + // as dictated by the implementation of WindowCompat. + View view = window.getDecorView(); + view.setSystemUiVisibility( + view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } } } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 59733b29bd0b0..25c65d1508d0b 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -20,6 +20,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.PlatformChannel; @@ -28,14 +30,13 @@ /** Android implementation of the platform plugin. */ public class PlatformPlugin { - public static final int DEFAULT_SYSTEM_UI = - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; private final Activity activity; private final PlatformChannel platformChannel; private final PlatformPluginDelegate platformPluginDelegate; private PlatformChannel.SystemChromeStyle currentTheme; - private int mEnabledOverlays; + private PlatformChannel.SystemUiMode currentSystemUiMode; + private List currentOverlays; private static final String TAG = "PlatformPlugin"; /** @@ -140,8 +141,6 @@ public PlatformPlugin( this.platformChannel = platformChannel; this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler); this.platformPluginDelegate = delegate; - - mEnabledOverlays = DEFAULT_SYSTEM_UI; } /** @@ -238,17 +237,75 @@ public void onSystemUiVisibilityChange(int visibility) { } private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode systemUiMode) { + if (Build.VERSION.SDK_INT <= 19) { + // As of API 30, the Android APIs for overlays/insets provides backwards compatibility back + // through API 19. Since Flutter currently supports API 19, the legacy code is used for + // that case. + setSystemChromeEnabledSystemUIModeLegacy(systemUiMode); + } else { + Window window = activity.getWindow(); + View view = window.getDecorView(); + WindowInsetsControllerCompat windowInsetsControllerCompat = + new WindowInsetsControllerCompat(window, view); + + if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK) { + // LEAN BACK + // Available starting at SDK 16. Implemented for API 20+ here. + // Should not show overlays, tap to reveal overlays, needs onChange callback + // When the overlays come in on tap, the app does not receive 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. + windowInsetsControllerCompat.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH); + windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); + WindowCompat.setDecorFitsSystemWindows(window, false); + } else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE) { + // IMMERSIVE + // Available starting at SDK 19. Implemented for API 20+ here. + // 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. + windowInsetsControllerCompat.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE); + windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); + WindowCompat.setDecorFitsSystemWindows(window, false); + } else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE_STICKY) { + // STICKY IMMERSIVE + // Available starting at SDK 19. Implemented for API 20+ here. + // 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. + windowInsetsControllerCompat.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); + WindowCompat.setDecorFitsSystemWindows(window, false); + } else if (systemUiMode == PlatformChannel.SystemUiMode.EDGE_TO_EDGE + && Build.VERSION.SDK_INT >= 29) { + // EDGE TO EDGE + // Available starting at SDK 29. See issue for context: + // https://github.com/flutter/flutter/issues/89774. + // Will apply a translucent body scrim behind 2/3 button navigation bars + // to ensure contrast with buttons on the nav and status bars, unless the contrast is not + // enforced in the overlay styling. + WindowCompat.setDecorFitsSystemWindows(window, false); + } else { + // When none of the conditions are matched, return without updating the system UI overlays. + return; + } + } + currentSystemUiMode = systemUiMode; + } + + private void setSystemChromeEnabledSystemUIModeLegacy(PlatformChannel.SystemUiMode systemUiMode) { int enabledOverlays; - if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK) { // 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 receive 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. + // Available starting at SDK 16. Implemented for APIs 16-19 here. enabledOverlays = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION @@ -258,12 +315,8 @@ private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode sys } 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. + // Available starting at SDK 19. Implemented for API 19 here. Earlier versions will not be + // affected by this. enabledOverlays = View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE @@ -274,11 +327,8 @@ private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode sys } 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. + // Available starting at SDK 19. Implemented for API 19 here. Earlier versions will not be + // affected by this. enabledOverlays = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE @@ -286,44 +336,69 @@ private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode sys | 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 >= 29) { - // EDGE TO EDGE - // Available starting at 29 - // SDK 29 and up will apply a translucent body scrim behind 2/3 button navigation bars - // to ensure contrast with buttons on the nav and status bars, unless the contrast is not - // enforced in the overlay styling. - enabledOverlays = - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; } else { - // When none of the conditions are matched, return without updating the system UI overlays. return; } - - mEnabledOverlays = enabledOverlays; - updateSystemUiOverlays(); + activity.getWindow().getDecorView().setSystemUiVisibility(enabledOverlays); } private void setSystemChromeEnabledSystemUIOverlays( List overlaysToShow) { - // Start by assuming we want to hide all system overlays (like an immersive - // game). + if (Build.VERSION.SDK_INT <= 19) { + // As of API 30, the Android APIs for overlays/insets provides backwards compatibility back + // through API 19. Since Flutter currently supports API 19, the legacy code is used + // for that case. + setSystemChromeEnabledSystemUIOverlaysLegacy(overlaysToShow); + } else { + Window window = activity.getWindow(); + View view = window.getDecorView(); + WindowInsetsControllerCompat windowInsetsControllerCompat = + new WindowInsetsControllerCompat(window, view); + + // Start by assuming we want to hide all system overlays (like an immersive + // game). + windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); + WindowCompat.setDecorFitsSystemWindows(window, false); + + // We apply sticky immersive mode if desired. Available starting at SDK 20. + if (overlaysToShow.size() == 0) { + currentSystemUiMode = PlatformChannel.SystemUiMode.IMMERSIVE_STICKY; + windowInsetsControllerCompat.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + + // Re-add any desired system overlays. + for (int i = 0; i < overlaysToShow.size(); ++i) { + PlatformChannel.SystemUiOverlay overlayToShow = overlaysToShow.get(i); + switch (overlayToShow) { + case TOP_OVERLAYS: + windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars()); + break; + case BOTTOM_OVERLAYS: + windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars()); + break; + } + } + } + currentOverlays = overlaysToShow; + } + + private void setSystemChromeEnabledSystemUIOverlaysLegacy( + List overlaysToShow) { int enabledOverlays = - DEFAULT_SYSTEM_UI + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we - // apply it - // if desired, and if the current Android version is 19 or greater. + // apply it if desired. if (overlaysToShow.size() == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + currentSystemUiMode = PlatformChannel.SystemUiMode.IMMERSIVE_STICKY; enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } - // Re-add any desired system overlays. for (int i = 0; i < overlaysToShow.size(); ++i) { PlatformChannel.SystemUiOverlay overlayToShow = overlaysToShow.get(i); switch (overlayToShow) { @@ -336,9 +411,7 @@ private void setSystemChromeEnabledSystemUIOverlays( break; } } - - mEnabledOverlays = enabledOverlays; - updateSystemUiOverlays(); + activity.getWindow().getDecorView().setSystemUiVisibility(enabledOverlays); } /** @@ -350,9 +423,24 @@ private void setSystemChromeEnabledSystemUIOverlays( * PlatformPlugin}. */ public void updateSystemUiOverlays() { - activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays); - if (currentTheme != null) { - setSystemChromeSystemUIOverlayStyle(currentTheme); + if (currentOverlays != null) { + setSystemChromeEnabledSystemUIOverlays(currentOverlays); + + if (currentOverlays.size() > 0) { + if (currentSystemUiMode != null) { + setSystemChromeEnabledSystemUIMode(currentSystemUiMode); + } else { + Window window = activity.getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < 30) { + // This ensures that the navigation bar is not hidden for APIs < 30, + // as dictated by the implementation of WindowCompat. + View view = window.getDecorView(); + view.setSystemUiVisibility( + view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + } + } } } 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 2a04684aa6ab2..ea8448363ca23 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -13,6 +13,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; @@ -24,6 +25,8 @@ import android.view.Window; import android.view.WindowInsetsController; import androidx.activity.OnBackPressedCallback; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; import androidx.fragment.app.FragmentActivity; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.systemchannels.PlatformChannel; @@ -32,6 +35,8 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel.SystemChromeStyle; import io.flutter.plugin.platform.PlatformPlugin.PlatformPluginDelegate; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; @@ -313,7 +318,9 @@ public void setStatusBarIconBrightness() { @Config(sdk = 29) @Test - public void setSystemUiMode() { + public void setSystemUiModeLegacy() { + // This test reflects the behavior under the hood of the Android overlay/inset APIs used for API + // 20-29 in the plugin. View fakeDecorView = mock(View.class); Window fakeWindow = mock(Window.class); when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); @@ -325,41 +332,42 @@ public void setSystemUiMode() { if (Build.VERSION.SDK_INT >= 28) { platformPlugin.mPlatformMessageHandler.showSystemUiMode( PlatformChannel.SystemUiMode.LEAN_BACK); + + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); verify(fakeDecorView) .setSystemUiVisibility( 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); + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); platformPlugin.mPlatformMessageHandler.showSystemUiMode( PlatformChannel.SystemUiMode.IMMERSIVE); - verify(fakeDecorView) + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE); + verify(fakeDecorView, times(2)).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView, times(2)).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + verify(fakeDecorView, times(2)) .setSystemUiVisibility( - View.SYSTEM_UI_FLAG_IMMERSIVE - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + 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); + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); platformPlugin.mPlatformMessageHandler.showSystemUiMode( PlatformChannel.SystemUiMode.IMMERSIVE_STICKY); - verify(fakeDecorView) + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + verify(fakeDecorView, times(3)).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView, times(3)).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + verify(fakeDecorView, times(3)) .setSystemUiVisibility( - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + 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); + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } if (Build.VERSION.SDK_INT >= 29) { platformPlugin.mPlatformMessageHandler.showSystemUiMode( PlatformChannel.SystemUiMode.EDGE_TO_EDGE); - verify(fakeDecorView) + verify(fakeDecorView, times(4)) .setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION @@ -367,6 +375,136 @@ public void setSystemUiMode() { } } + @TargetApi(30) + @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); + WindowInsetsController fakeWindowInsetsController = mock(WindowInsetsController.class); + when(fakeWindow.getInsetsController()).thenReturn(fakeWindowInsetsController); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode(PlatformChannel.SystemUiMode.LEAN_BACK); + + verify(fakeWindowInsetsController) + .setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH); + verify(fakeWindowInsetsController).hide(WindowInsetsCompat.Type.systemBars()); + verify(fakeWindow).setDecorFitsSystemWindows(false); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode(PlatformChannel.SystemUiMode.IMMERSIVE); + + verify(fakeWindowInsetsController) + .setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE); + verify(fakeWindowInsetsController, times(2)).hide(WindowInsetsCompat.Type.systemBars()); + verify(fakeWindow, times(2)).setDecorFitsSystemWindows(false); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.IMMERSIVE_STICKY); + + verify(fakeWindowInsetsController) + .setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + verify(fakeWindowInsetsController, times(3)).hide(WindowInsetsCompat.Type.systemBars()); + verify(fakeWindow, times(3)).setDecorFitsSystemWindows(false); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.EDGE_TO_EDGE); + + verify(fakeWindow, times(4)).setDecorFitsSystemWindows(false); + } + + @Config(sdk = 29) + @Test + public void showSystemOverlaysLegacy() { + // This test reflects the behavior under the hood of the Android overlay/inset APIs used for API + // 20-29 in the plugin. + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + int fakeSetFlags = + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + platformPlugin.mPlatformMessageHandler.showSystemOverlays( + new ArrayList()); + + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + verify(fakeDecorView) + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + verify(fakeDecorView) + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + when(fakeDecorView.getSystemUiVisibility()).thenReturn(fakeSetFlags); + platformPlugin.mPlatformMessageHandler.showSystemOverlays( + new ArrayList( + Arrays.asList( + PlatformChannel.SystemUiOverlay.TOP_OVERLAYS, + PlatformChannel.SystemUiOverlay.BOTTOM_OVERLAYS))); + + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView).setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + verify(fakeDecorView) + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + verify(fakeDecorView).setSystemUiVisibility(fakeSetFlags &= ~View.SYSTEM_UI_FLAG_FULLSCREEN); + verify(fakeDecorView) + .setSystemUiVisibility(fakeSetFlags &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + } + + @TargetApi(30) + @Test + public void showSystemOverlays() { + 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); + WindowInsetsController fakeWindowInsetsController = mock(WindowInsetsController.class); + when(fakeWindow.getInsetsController()).thenReturn(fakeWindowInsetsController); + + platformPlugin.mPlatformMessageHandler.showSystemOverlays( + new ArrayList()); + + verify(fakeWindowInsetsController) + .setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + verify(fakeWindowInsetsController).hide(WindowInsetsCompat.Type.systemBars()); + + platformPlugin.mPlatformMessageHandler.showSystemOverlays( + new ArrayList( + Arrays.asList( + PlatformChannel.SystemUiOverlay.TOP_OVERLAYS, + PlatformChannel.SystemUiOverlay.BOTTOM_OVERLAYS))); + + verify(fakeWindowInsetsController).show(WindowInsetsCompat.Type.statusBars()); + verify(fakeWindowInsetsController).show(WindowInsetsCompat.Type.navigationBars()); + } + @Config(sdk = 28) @Test public void doNotEnableEdgeToEdgeOnOlderSdk() {