Fix systemBar flickering on API 31

On API 31, if an OnExitAnimationListener is set, the Window layout params are only
applied only when the [android.window.SplashScreenView] is removed. This lead to some
flickers.

To fix this, we apply these attributes to the window while the
[android.window.SplashScreenView] is still visible.

Bug: 196273921
Test: SplashscreenParametrizedTest#endStateStableWithAndWithoutListener
Change-Id: I7e0593caba49563ccf06892df7f294e43ead7a5d
diff --git a/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml b/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml
new file mode 100644
index 0000000..01f4921
--- /dev/null
+++ b/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<!-- Debug drawable to identify the top and bottom of the content view without coloring
+the whole background -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:width="40dp"
+        android:left="40dp">
+        <color android:color="#0000FF" />
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml b/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
index e170fbe..a157632 100644
--- a/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
+++ b/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
@@ -19,6 +19,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:gravity="center_horizontal"
+    android:background="@drawable/vertical_line"
     android:orientation="vertical">
 
     <LinearLayout
diff --git a/core/core-splashscreen/src/androidTest/AndroidManifest.xml b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
index e1fabc9..8414ca5 100644
--- a/core/core-splashscreen/src/androidTest/AndroidManifest.xml
+++ b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
@@ -30,7 +30,7 @@
         <activity
             android:name=".SplashScreenAppCompatTestActivity"
             android:exported="true"
-            android:theme="@style/Theme.Test.Starting.AppCompat">
+            android:theme="@style/Theme.Test.AppCompat.Starting">
         </activity>
 
         <activity
@@ -39,5 +39,17 @@
             android:theme="@style/Theme.Test.Starting.IconBackground">
         </activity>
 
+        <activity
+            android:name=".SplashScreenStability1"
+            android:exported="true"
+            android:theme="@style/Theme.Test.Stability1.Starting">
+        </activity>
+
+        <activity
+            android:name=".SplashScreenStability2"
+            android:exported="true"
+            android:theme="@style/Theme.Test.Stability2.Starting">
+        </activity>
+
     </application>
 </manifest>
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
index adf7e72..0173146 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
@@ -18,6 +18,7 @@
 import android.app.Activity
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
 
 open class SplashScreenTestActivity : Activity(), SplashScreenTestControllerHolder {
 
@@ -43,3 +44,17 @@
 
     override lateinit var controller: SplashScreenTestController
 }
+
+class SplashScreenStability1 : SplashScreenTestActivity() {
+    override fun onStart() {
+        super.onStart()
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+    }
+}
+
+class SplashScreenStability2 : SplashScreenTestActivity() {
+    override fun onStart() {
+        super.onStart()
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+    }
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index 3418529..3fe8b23 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -25,6 +25,8 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.screenshot.ScreenCapture
+import androidx.test.runner.screenshot.Screenshot
 import androidx.test.screenshot.matchers.MSSIMMatcher
 import androidx.test.uiautomator.UiDevice
 import org.junit.Assert.assertEquals
@@ -160,6 +162,75 @@
     }
 
     /**
+     * Checks that activity and especially the system bars, have the same appearance whether we set
+     * an OnExitAnimationListener or not. This allows us to check that the system ui is stable
+     * before and after the removal of the SplashScreenView.
+     */
+    @Test
+    fun endStateStableWithAndWithoutListener() {
+        // Take a screenshot of the activity when no OnExitAnimationListener is set.
+        // This is our reference.
+        var controller = startActivityWithSplashScreen(SplashScreenStability1::class, device) {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, false)
+        }
+        assertTrue(controller.drawnLatch.await(2, TimeUnit.SECONDS))
+        Thread.sleep(500)
+        val withoutListener = Screenshot.capture()
+
+        // Take a screenshot of the container view while the splash screen view is invisible but
+        // not removed
+        controller = startActivityWithSplashScreen(SplashScreenStability1::class, device) {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+            it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+        }
+        val withListener = screenshotContainerInExitListener(controller)
+
+        compareBitmaps(withoutListener.bitmap, withListener.bitmap, 0.999)
+
+        // Execute the same steps as above but with another set of theme attributes to check.
+        controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, false)
+        }
+        controller.waitForActivityDrawn()
+        Thread.sleep(500)
+        val withoutListener2 = Screenshot.capture()
+
+        controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+            it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+        }
+        val withListener2 = screenshotContainerInExitListener(controller)
+        compareBitmaps(withListener2.bitmap, withoutListener2.bitmap)
+    }
+
+    private fun screenshotContainerInExitListener(
+        controller: SplashScreenTestController
+    ): ScreenCapture {
+        lateinit var contentViewInListener: ScreenCapture
+        controller.doOnExitAnimation {
+            it.view.visibility = View.INVISIBLE
+            it.view.postDelayed({
+                contentViewInListener = Screenshot.capture()
+                it.remove()
+                controller.exitAnimationListenerLatch.countDown()
+            }, 100)
+            true
+        }
+        controller.waitBarrier.set(false)
+        controller.waitSplashScreenViewRemoved()
+        controller.activity.finishAndRemoveTask()
+        return contentViewInListener
+    }
+
+    /**
      * The splash screen is drawn full screen. On Android 12, this is achieved using
      * [Window.setDecorFitsSystemWindow(false)].
      */
@@ -203,7 +274,8 @@
 
     private fun compareBitmaps(
         beforeScreenshot: Bitmap,
-        afterScreenshot: Bitmap
+        afterScreenshot: Bitmap,
+        threshold: Double = 0.99
     ) {
         val beforeBuffer = IntArray(beforeScreenshot.width * beforeScreenshot.height)
         beforeScreenshot.getPixels(
@@ -217,7 +289,7 @@
             afterScreenshot.width, afterScreenshot.height
         )
 
-        val matcher = MSSIMMatcher(0.99).compareBitmaps(
+        val matcher = MSSIMMatcher(threshold).compareBitmaps(
             beforeBuffer, afterBuffer, afterScreenshot.width,
             afterScreenshot.height
         )
@@ -285,4 +357,18 @@
             activityClass, device, intentModifier
         )
     }
+
+    private fun SplashScreenTestController.waitForActivityDrawn() {
+        assertTrue(
+            "Activity was never drawn",
+            drawnLatch.await(2, TimeUnit.SECONDS)
+        )
+    }
+
+    private fun SplashScreenTestController.waitSplashScreenViewRemoved() {
+        assertTrue(
+            "Exit animation listener was not called",
+            exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
+        )
+    }
 }
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
index 6570777..b5010c1 100644
--- a/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
+++ b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
@@ -19,6 +19,6 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:background="#00FF00">
+    android:background="#FEDEDE">
 
 </androidx.core.splashscreen.test.TestView>
diff --git a/core/core-splashscreen/src/androidTest/res/values/styles.xml b/core/core-splashscreen/src/androidTest/res/values/styles.xml
index 4a9e72b..7fabf0a 100644
--- a/core/core-splashscreen/src/androidTest/res/values/styles.xml
+++ b/core/core-splashscreen/src/androidTest/res/values/styles.xml
@@ -16,11 +16,28 @@
 
 <resources>
 
-    <style name="Theme.Test" parent="android:Theme.Material.Light.NoActionBar">
+    <!--
+     Two important point that we care about in the tests:
+     1. That the SplashScreen View, used for the OnExitAnimationListener is the same as the system
+     provided splash screen window.
+     2. That for the system bars, the transition between the system's theme and the app themes is
+     smooth and happens when the app is drawn.
+
+    Because handling of system bars colors differs between API, tests related to the SplashScreen
+    View and doing screenshot comparison are using themes with fixed system bar style, hence the
+    presence of the windowLight*Bar attribute.
+
+    The Stability* tests and associated themes are testing the expected behavior when the system
+    bars have different values on the splash screen theme and the app theme.
+    -->
+
+    <style name="Theme.Test" parent="android:Theme.DeviceDefault.Light.NoActionBar">
         <item name="android:windowDrawsSystemBarBackgrounds">true</item>
         <item name="android:fitsSystemWindows">false</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:windowLightStatusBar">true</item>
     </style>
 
     <style name="Theme.Test.Starting" parent="Theme.SplashScreen">
@@ -28,6 +45,8 @@
         <item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
         <item name="windowSplashScreenAnimationDuration">1234</item>
         <item name="postSplashScreenTheme">@style/Theme.Test</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:windowLightStatusBar">true</item>
     </style>
 
     <style name="Theme.Test.Starting.IconBackground" parent="Theme.SplashScreen.IconBackground">
@@ -36,6 +55,8 @@
         <item name="windowSplashScreenAnimationDuration">1234</item>
         <item name="windowSplashScreenIconBackgroundColor">@color/icon_bg</item>
         <item name="postSplashScreenTheme">@style/Theme.Test</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:windowLightStatusBar">true</item>
     </style>
 
     <!-- Themes for AppCompat Tests -->
@@ -44,13 +65,47 @@
         <item name="android:fitsSystemWindows">false</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:windowLightStatusBar">true</item>
     </style>
 
-    <style name="Theme.Test.Starting.AppCompat" parent="Theme.SplashScreen">
+    <style name="Theme.Test.AppCompat.Starting" parent="Theme.SplashScreen">
         <item name="windowSplashScreenBackground">@color/bg_launcher</item>
         <item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
         <item name="windowSplashScreenAnimationDuration">1234</item>
         <item name="postSplashScreenTheme">@style/Theme.Test.AppCompat</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:windowLightStatusBar">true</item>
+    </style>
+
+    <!-- Themes for system ui stability -->
+    <style name="Theme.Test.Stability1.Starting" parent="Theme.SplashScreen">
+        <item name="postSplashScreenTheme">@style/Theme.Test.Stability1</item>
+    </style>
+
+    <style name="Theme.Test.Stability2.Starting" parent="Theme.SplashScreen">
+        <item name="postSplashScreenTheme">@style/Theme.Test.Stability2</item>
+    </style>
+
+    <style name="Theme.Test.Stability1" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:fitsSystemWindows">false</item>
+        <item name="android:statusBarColor">#AAFF0000</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:enforceNavigationBarContrast">true</item>
+        <item name="android:enforceStatusBarContrast">true</item>
+        <item name="android:windowLightStatusBar">true</item>
+        <item name="android:windowLightNavigationBar">true</item>
+    </style>
+
+    <style name="Theme.Test.Stability2" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:fitsSystemWindows">false</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">#AAFF0000</item>
+        <item name="android:enforceStatusBarContrast">true</item>
+        <item name="android:windowLightStatusBar">false</item>
+        <item name="android:windowLightNavigationBar">false</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
index 1fc3ebe..ac95d97 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -16,6 +16,7 @@
 
 package androidx.core.splashscreen
 
+import android.R.attr
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.res.Resources
@@ -29,10 +30,12 @@
 import android.view.ViewGroup
 import android.view.ViewTreeObserver.OnPreDrawListener
 import android.view.WindowInsets
+import android.view.WindowManager.LayoutParams
 import android.widget.ImageView
 import android.window.SplashScreenView
 import androidx.annotation.MainThread
 import androidx.annotation.RequiresApi
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition
 
 /**
@@ -456,11 +459,51 @@
             }
         }
 
+        /**
+         * Apply the system ui related theme attribute defined in the application to override the
+         * ones set on the [SplashScreenView]
+         *
+         * On API 31, if an OnExitAnimationListener is set, the Window layout params are only
+         * applied only when the [SplashScreenView] is removed. This lead to some
+         * flickers.
+         *
+         * To fix this, we apply these attributes as soon as the [SplashScreenView]
+         * is visible.
+         */
         private fun applyAppSystemUiTheme() {
+            val tv = TypedValue()
+            val theme = activity.theme
             val window = activity.window
 
+            if (theme.resolveAttribute(attr.statusBarColor, tv, true)) {
+                window.statusBarColor = tv.data
+            }
+
+            if (theme.resolveAttribute(attr.navigationBarColor, tv, true)) {
+                window.navigationBarColor = tv.data
+            }
+
+            if (theme.resolveAttribute(attr.windowDrawsSystemBarBackgrounds, tv, true)) {
+                if (tv.data != 0) {
+                    window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+                } else {
+                    window.clearFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+                }
+            }
+
+            if (theme.resolveAttribute(attr.enforceNavigationBarContrast, tv, true)) {
+                window.isNavigationBarContrastEnforced = tv.data != 0
+            }
+
+            if (theme.resolveAttribute(attr.enforceStatusBarContrast, tv, true)) {
+                window.isStatusBarContrastEnforced = tv.data != 0
+            }
+
+            val decorView = window.decorView as ViewGroup
+            ThemeUtils.Api31.applyThemesSystemBarAppearance(theme, decorView, tv)
+
             // Fix setDecorFitsSystemWindows being overridden by the SplashScreenView
-            (activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(null)
+            decorView.setOnHierarchyChangeListener(null)
             window.setDecorFitsSystemWindows(mDecorFitWindowInsets)
         }
     }
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
index a0c4e5b..1eae14c 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
@@ -132,6 +132,12 @@
         override val iconAnimationDurationMillis: Long
             get() = platformView.iconAnimationDuration?.toMillis() ?: 0
 
-        override fun remove() = platformView.remove()
+        override fun remove() {
+            platformView.remove()
+            ThemeUtils.Api31.applyThemesSystemBarAppearance(
+                activity.theme,
+                activity.window.decorView
+            )
+        }
     }
 }
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt
new file mode 100644
index 0000000..66d08c6
--- /dev/null
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.splashscreen
+
+import android.content.res.Resources
+import android.util.TypedValue
+import android.view.View
+import android.view.WindowInsetsController
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+/**
+ * Utility function for applying themes fixes to the system bar when the [SplashScreenViewProvider]
+ * is added and removed.
+ */
+@RequiresApi(31)
+internal object ThemeUtils {
+    object Api31 {
+
+        /**
+         * Apply the theme's values for the system bar appearance to the decorView.
+         *
+         * This needs to be called when the [SplashScreenViewProvider] is added and after it's been
+         * removed.
+         */
+        @JvmStatic
+        @JvmOverloads
+        @DoNotInline
+        fun applyThemesSystemBarAppearance(
+            theme: Resources.Theme,
+            decor: View,
+            tv: TypedValue = TypedValue()
+        ) {
+            var appearance = 0
+            val mask =
+                WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
+                    WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+            if (theme.resolveAttribute(android.R.attr.windowLightStatusBar, tv, true)) {
+                if (tv.data != 0) {
+                    appearance = appearance or WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+                }
+            }
+            if (theme.resolveAttribute(android.R.attr.windowLightNavigationBar, tv, true)) {
+                if (tv.data != 0) {
+                    appearance =
+                        appearance or WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+                }
+            }
+            decor.windowInsetsController!!.setSystemBarsAppearance(appearance, mask)
+        }
+    }
+}
\ No newline at end of file