diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java index 0fe692c49c6c7..846fb97083628 100644 --- a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -136,6 +136,7 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { } @Override + @SuppressWarnings("deprecation") public void onCreate(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = activity.getWindow(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 4d16a2ddab38d..89258a7068716 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -614,6 +614,7 @@ private View createFlutterView() { /*shouldDelayFirstAndroidViewDraw=*/ getRenderMode() == RenderMode.surface); } + @SuppressWarnings("deprecation") private void configureStatusBarForFullscreenFlutterExperience() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = getWindow(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index d2034b12ac67a..4250a206b5f72 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -515,6 +515,7 @@ protected FlutterFragment createFlutterFragment() { } } + @SuppressWarnings("deprecation") private void configureStatusBarForFullscreenFlutterExperience() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = getWindow(); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index a7d1c628b3c26..8439c8ff3ed23 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,6 +30,7 @@ /** Android implementation of the platform plugin. */ public class PlatformPlugin { + @SuppressWarnings("deprecation") public static final int DEFAULT_SYSTEM_UI = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; @@ -35,7 +38,8 @@ public class PlatformPlugin { 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"; /** @@ -142,8 +146,6 @@ public PlatformPlugin( this.platformChannel = platformChannel; this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler); this.platformPluginDelegate = delegate; - - mEnabledOverlays = DEFAULT_SYSTEM_UI; } /** @@ -241,17 +243,76 @@ 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; + } + + @SuppressWarnings("deprecation") + 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 @@ -261,12 +322,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 @@ -277,11 +334,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 @@ -289,44 +343,70 @@ 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; + } + + @SuppressWarnings("deprecation") + 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) { @@ -339,9 +419,7 @@ private void setSystemChromeEnabledSystemUIOverlays( break; } } - - mEnabledOverlays = enabledOverlays; - updateSystemUiOverlays(); + activity.getWindow().getDecorView().setSystemUiVisibility(enabledOverlays); } /** @@ -352,8 +430,27 @@ private void setSystemChromeEnabledSystemUIOverlays( * Window} associated with the {@link android.app.Activity} that was provided to this {@code * PlatformPlugin}. */ + @SuppressWarnings("deprecation") public void updateSystemUiOverlays() { - activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays); + 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); + } + } + } + } if (currentTheme != null) { setSystemChromeSystemUIOverlayStyle(currentTheme); } 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 b01eab04d4878..9c9d2d3ffbe9a 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -6,6 +6,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -13,6 +15,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; @@ -25,6 +28,8 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import androidx.activity.OnBackPressedCallback; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; import androidx.fragment.app.FragmentActivity; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -34,6 +39,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; @@ -314,7 +321,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); @@ -326,41 +335,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 @@ -368,6 +378,173 @@ 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 = 30) + @Test + public void verifyUpdateSystemUiOverlaysAppliesCurrentTheme() { + 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); + + // Style that requires usage of all system bar APIs used in PlatformPlugin to update overlay + // style + SystemChromeStyle testStyle = + new SystemChromeStyle( + 0XFF000000, Brightness.LIGHT, true, 0XFFC70039, Brightness.LIGHT, 0XFF006DB3, true); + + platformPlugin.updateSystemUiOverlays(); + + verify(fakeWindow, never()).setStatusBarColor(anyInt()); + verify(fakeWindow, never()).setNavigationBarColor(anyInt()); + verify(fakeWindow, never()).setNavigationBarDividerColor(anyInt()); + verify(fakeWindow, never()).setStatusBarContrastEnforced(anyBoolean()); + verify(fakeWindow, never()).setNavigationBarContrastEnforced(anyBoolean()); + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(testStyle); + platformPlugin.updateSystemUiOverlays(); + + verify(fakeWindow, times(2)).setStatusBarColor(0xFF000000); + verify(fakeWindow, times(2)).setNavigationBarColor(0XFFC70039); + verify(fakeWindow, times(2)).setNavigationBarDividerColor(0XFF006DB3); + verify(fakeWindow, times(2)).setStatusBarContrastEnforced(true); + verify(fakeWindow, times(2)).setNavigationBarContrastEnforced(true); + } + @Config(sdk = 28) @Test public void doNotEnableEdgeToEdgeOnOlderSdk() {