Merge "Rename layout -> blueprint and add "sections"" into udc-qpr-dev
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index df8a50a..4fc90ae 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -16,6 +16,7 @@
 
 package android.animation;
 
+import android.annotation.Nullable;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
@@ -91,9 +92,13 @@
     };
 
     public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+    private static AnimationHandler sTestHandler = null;
     private boolean mListDirty = false;
 
     public static AnimationHandler getInstance() {
+        if (sTestHandler != null) {
+            return sTestHandler;
+        }
         if (sAnimatorHandler.get() == null) {
             sAnimatorHandler.set(new AnimationHandler());
         }
@@ -101,6 +106,17 @@
     }
 
     /**
+     * Sets an instance that will be returned by {@link #getInstance()} on every thread.
+     * @return  the previously active test handler, if any.
+     * @hide
+     */
+    public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) {
+        AnimationHandler oldHandler = sTestHandler;
+        sTestHandler = handler;
+        return oldHandler;
+    }
+
+    /**
      * System property that controls the behavior of pausing infinite animators when an app
      * is moved to the background.
      *
@@ -369,7 +385,10 @@
      * Return the number of callbacks that have registered for frame callbacks.
      */
     public static int getAnimationCount() {
-        AnimationHandler handler = sAnimatorHandler.get();
+        AnimationHandler handler = sTestHandler;
+        if (handler == null) {
+            handler = sAnimatorHandler.get();
+        }
         if (handler == null) {
             return 0;
         }
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index badad58..28b5870 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -192,7 +192,7 @@
     <string name="kg_prompt_after_user_lockdown_pattern">Pattern is required after lockdown</string>
 
     <!-- Message shown to prepare for an unattended update (OTA). Also known as an over-the-air (OTA) update.  [CHAR LIMIT=70] -->
-    <string name="kg_prompt_unattended_update">Update will install during inactive hours</string>
+    <string name="kg_prompt_unattended_update">Update will install when device not in use</string>
 
     <!-- Message shown when primary authentication hasn't been used for some time.  [CHAR LIMIT=70] -->
     <string name="kg_prompt_pin_auth_timeout">Added security required. PIN not used for a while.</string>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9c864ab..b6ef594 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -681,7 +681,6 @@
         <item>6</item> <!-- WAKE_REASON_WAKE_KEY -->
         <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
         <item>9</item> <!-- WAKE_REASON_LID -->
-        <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
         <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
         <item>15</item> <!-- WAKE_REASON_TAP -->
         <item>16</item> <!-- WAKE_REASON_LIFT -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index bb11217..b81e081 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -178,6 +178,7 @@
                 getKeyguardSecurityCallback().dismiss(true, userId, getSecurityMode());
             }
         } else {
+            mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */);
             if (isValidPassword) {
                 getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
                 if (timeoutMs > 0) {
@@ -186,7 +187,6 @@
                     handleAttemptLockout(deadline);
                 }
             }
-            mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */);
             if (timeoutMs == 0) {
                 mMessageAreaController.setMessage(mView.getWrongPasswordStringId());
             }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 3b09910f..aff2591 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -27,6 +27,7 @@
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER;
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES;
 
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
@@ -1081,8 +1082,10 @@
         mLockPatternUtils.reportFailedPasswordAttempt(userId);
         if (timeoutMs > 0) {
             mLockPatternUtils.reportPasswordLockout(timeoutMs, userId);
-            mView.showTimeoutDialog(userId, timeoutMs, mLockPatternUtils,
-                    mSecurityModel.getSecurityMode(userId));
+            if (!mFeatureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) {
+                mView.showTimeoutDialog(userId, timeoutMs, mLockPatternUtils,
+                        mSecurityModel.getSecurityMode(userId));
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 53229b9..d950c917 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2696,14 +2696,6 @@
     }
 
     /**
-     * @return true if the FP sensor is non-UDFPS and the device can be unlocked using fingerprint
-     * at this moment.
-     */
-    public boolean isFingerprintAllowedInBouncer() {
-        return !isUdfpsSupported() && isUnlockingWithFingerprintAllowed();
-    }
-
-    /**
      * @return true if there's at least one sfps enrollment for the current user.
      */
     public boolean isSfpsEnrolled() {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
index 3206c00..1817ea9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
@@ -32,6 +32,7 @@
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
@@ -53,6 +54,9 @@
 import com.android.systemui.R.string.kg_primary_auth_locked_out_pattern
 import com.android.systemui.R.string.kg_primary_auth_locked_out_pin
 import com.android.systemui.R.string.kg_prompt_after_dpm_lock
+import com.android.systemui.R.string.kg_prompt_after_update_password
+import com.android.systemui.R.string.kg_prompt_after_update_pattern
+import com.android.systemui.R.string.kg_prompt_after_update_pin
 import com.android.systemui.R.string.kg_prompt_after_user_lockdown_password
 import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pattern
 import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
@@ -89,69 +93,76 @@
     fun createFromPromptReason(
         @BouncerPromptReason reason: Int,
         userId: Int,
+        secondaryMsgOverride: String? = null
     ): BouncerMessageModel? {
         val pair =
             getBouncerMessage(
                 reason,
                 securityModel.getSecurityMode(userId),
-                updateMonitor.isFingerprintAllowedInBouncer
+                updateMonitor.isUnlockingWithFingerprintAllowed
             )
         return pair?.let {
             BouncerMessageModel(
-                message = Message(messageResId = pair.first),
-                secondaryMessage = Message(messageResId = pair.second)
+                message = Message(messageResId = pair.first, animate = false),
+                secondaryMessage =
+                    secondaryMsgOverride?.let {
+                        Message(message = secondaryMsgOverride, animate = false)
+                    }
+                        ?: Message(messageResId = pair.second, animate = false)
             )
         }
     }
 
-    fun createFromString(
-        primaryMsg: String? = null,
-        secondaryMsg: String? = null
-    ): BouncerMessageModel =
-        BouncerMessageModel(
-            message = primaryMsg?.let { Message(message = it) },
-            secondaryMessage = secondaryMsg?.let { Message(message = it) },
-        )
-
     /**
      * Helper method that provides the relevant bouncer message that should be shown for different
-     * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
+     * scenarios indicated by [reason]. [securityMode] & [fpAuthIsAllowed] parameters are used to
      * provide a more specific message.
      */
     private fun getBouncerMessage(
         @BouncerPromptReason reason: Int,
         securityMode: SecurityMode,
-        fpAllowedInBouncer: Boolean = false
+        fpAuthIsAllowed: Boolean = false
     ): Pair<Int, Int>? {
         return when (reason) {
+            // Primary auth locked out
+            PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
+            // Primary auth required reasons
             PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
             PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
             PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
             PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
-            PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
             PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
+            PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE -> authRequiredForMainlineUpdate(securityMode)
             PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
-            PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
-            PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
-                if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
-                else incorrectSecurityInput(securityMode)
+            PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
+            // Non strong auth not available reasons
+            PROMPT_REASON_FACE_LOCKED_OUT ->
+                if (fpAuthIsAllowed) faceLockedOutButFingerprintAvailable(securityMode)
+                else faceLockedOut(securityMode)
             PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
-                if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
+                if (fpAuthIsAllowed) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
                 else nonStrongAuthTimeout(securityMode)
             PROMPT_REASON_TRUSTAGENT_EXPIRED ->
-                if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
+                if (fpAuthIsAllowed) trustAgentDisabledWithFingerprintAllowed(securityMode)
                 else trustAgentDisabled(securityMode)
+            // Auth incorrect input reasons.
+            PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
+                if (fpAuthIsAllowed) incorrectSecurityInputWithFingerprint(securityMode)
+                else incorrectSecurityInput(securityMode)
             PROMPT_REASON_INCORRECT_FACE_INPUT ->
-                if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
+                if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode)
                 else incorrectFaceInput(securityMode)
             PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
+            // Default message
             PROMPT_REASON_DEFAULT ->
-                if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
+                if (fpAuthIsAllowed) defaultMessageWithFingerprint(securityMode)
                 else defaultMessage(securityMode)
-            PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
             else -> null
         }
     }
+
+    fun emptyMessage(): BouncerMessageModel =
+        BouncerMessageModel(Message(message = ""), Message(message = ""))
 }
 
 @Retention(AnnotationRetention.SOURCE)
@@ -172,6 +183,7 @@
     PROMPT_REASON_NONE,
     PROMPT_REASON_RESTART,
     PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
+    PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE,
 )
 annotation class BouncerPromptReason
 
@@ -284,6 +296,15 @@
     }
 }
 
+private fun authRequiredForMainlineUpdate(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_update_pattern)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_update_password)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_update_pin)
+        else -> Pair(0, 0)
+    }
+}
+
 private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
     return when (securityMode) {
         SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout)
@@ -311,7 +332,7 @@
     }
 }
 
-private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
+private fun faceLockedOut(securityMode: SecurityMode): Pair<Int, Int> {
     return when (securityMode) {
         SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out)
         SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out)
@@ -320,6 +341,15 @@
     }
 }
 
+private fun faceLockedOutButFingerprintAvailable(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_face_locked_out)
+        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_face_locked_out)
+        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_face_locked_out)
+        else -> Pair(0, 0)
+    }
+}
+
 private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
     return when (securityMode) {
         SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
index 7e420cf..6fb0d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
@@ -28,6 +28,7 @@
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
@@ -38,6 +39,7 @@
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.TrustRepository
@@ -105,6 +107,9 @@
     fun clearMessage()
 }
 
+private const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
+private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"
+
 @SysUISingleton
 class BouncerMessageRepositoryImpl
 @Inject
@@ -114,6 +119,7 @@
     updateMonitor: KeyguardUpdateMonitor,
     private val bouncerMessageFactory: BouncerMessageFactory,
     private val userRepository: UserRepository,
+    private val systemPropertiesHelper: SystemPropertiesHelper,
     fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
 ) : BouncerMessageRepository {
 
@@ -132,6 +138,9 @@
     private val isAnyBiometricsEnabledAndEnrolled =
         or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
 
+    private val wasRebootedForMainlineUpdate
+        get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE
+
     private val authFlagsBasedPromptReason: Flow<Int> =
         combine(
                 biometricSettingsRepository.authenticationFlags,
@@ -144,7 +153,8 @@
                 return@map if (
                     trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
                 ) {
-                    PROMPT_REASON_RESTART
+                    if (wasRebootedForMainlineUpdate) PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
+                    else PROMPT_REASON_RESTART
                 } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
                     PROMPT_REASON_TIMEOUT
                 } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index d8cf398..8ed964d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -127,10 +127,12 @@
                 repository.setMessage(message ?: promptMessage(getAuthenticationMethod()))
                 sceneInteractor.setCurrentScene(
                     scene = SceneModel(SceneKey.Bouncer),
+                    loggingReason = "request to unlock device while authentication required",
                 )
             } else {
                 sceneInteractor.setCurrentScene(
                     scene = SceneModel(SceneKey.Gone),
+                    loggingReason = "request to unlock device while authentication isn't required",
                 )
             }
         }
@@ -176,6 +178,7 @@
         if (isAuthenticated) {
             sceneInteractor.setCurrentScene(
                 scene = SceneModel(SceneKey.Gone),
+                loggingReason = "successful authentication",
             )
         } else {
             repository.setMessage(errorMessage(getAuthenticationMethod()))
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index d06dd8e..fe01d08 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -86,7 +86,11 @@
 
         repository.setFingerprintAcquisitionMessage(
             if (value != null) {
-                factory.createFromString(secondaryMsg = value)
+                factory.createFromPromptReason(
+                    PROMPT_REASON_DEFAULT,
+                    userRepository.getSelectedUserInfo().id,
+                    secondaryMsgOverride = value
+                )
             } else {
                 null
             }
@@ -98,7 +102,11 @@
 
         repository.setFaceAcquisitionMessage(
             if (value != null) {
-                factory.createFromString(secondaryMsg = value)
+                factory.createFromPromptReason(
+                    PROMPT_REASON_DEFAULT,
+                    userRepository.getSelectedUserInfo().id,
+                    secondaryMsgOverride = value
+                )
             } else {
                 null
             }
@@ -110,7 +118,11 @@
 
         repository.setCustomMessage(
             if (value != null) {
-                factory.createFromString(secondaryMsg = value)
+                factory.createFromPromptReason(
+                    PROMPT_REASON_DEFAULT,
+                    userRepository.getSelectedUserInfo().id,
+                    secondaryMsgOverride = value
+                )
             } else {
                 null
             }
@@ -140,8 +152,7 @@
     // always maps to an empty string.
     private fun nullOrEmptyMessage() =
         flowOf(
-            if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
-            else factory.createFromString("", "")
+            if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null else factory.emptyMessage()
         )
 
     val bouncerMessage =
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
index 47fac2b..6486802 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
@@ -41,8 +41,6 @@
         super.onFinishInflate()
         primaryMessageView = findViewById(R.id.bouncer_primary_message_area)
         secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area)
-        primaryMessageView?.disable()
-        secondaryMessageView?.disable()
     }
 
     fun init(factory: KeyguardMessageAreaController.Factory) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index b5759e3..cc1504a 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -496,4 +496,12 @@
     public static LogBuffer provideDisplayMetricsRepoLogBuffer(LogBufferFactory factory) {
         return factory.create("DisplayMetricsRepo", 50);
     }
+
+    /** Provides a {@link LogBuffer} for the scene framework. */
+    @Provides
+    @SysUISingleton
+    @SceneFrameworkLog
+    public static LogBuffer provideSceneFrameworkLogBuffer(LogBufferFactory factory) {
+        return factory.create("SceneFramework", 50);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt
new file mode 100644
index 0000000..ef5f4e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 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 com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for the Scene Framework. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class SceneFrameworkLog
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 03bd11b..9f45f66 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -220,7 +220,7 @@
                             // If scene framework is enabled, set the scene container window to
                             // visible and let the touch "slip" into that window.
                             if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
-                                mSceneInteractor.get().setVisible(true);
+                                mSceneInteractor.get().setVisible(true, "swipe down on launcher");
                             } else {
                                 centralSurfaces.onInputFocusTransfer(
                                         mInputFocusTransferStarted, false /* cancel */,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index b09a5cf..64715bc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.shared.logger.SceneLogger
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.RemoteUserInput
 import com.android.systemui.scene.shared.model.SceneKey
@@ -41,6 +42,7 @@
 @Inject
 constructor(
     private val repository: SceneContainerRepository,
+    private val logger: SceneLogger,
 ) {
 
     /**
@@ -54,8 +56,17 @@
     }
 
     /** Sets the scene in the container with the given name. */
-    fun setCurrentScene(scene: SceneModel) {
+    fun setCurrentScene(scene: SceneModel, loggingReason: String) {
         val currentSceneKey = repository.currentScene.value.key
+        if (currentSceneKey == scene.key) {
+            return
+        }
+
+        logger.logSceneChange(
+            from = currentSceneKey,
+            to = scene.key,
+            reason = loggingReason,
+        )
         repository.setCurrentScene(scene)
         repository.setSceneTransition(from = currentSceneKey, to = scene.key)
     }
@@ -64,7 +75,17 @@
     val currentScene: StateFlow<SceneModel> = repository.currentScene
 
     /** Sets the visibility of the container with the given name. */
-    fun setVisible(isVisible: Boolean) {
+    fun setVisible(isVisible: Boolean, loggingReason: String) {
+        val wasVisible = repository.isVisible.value
+        if (wasVisible == isVisible) {
+            return
+        }
+
+        logger.logVisibilityChange(
+            from = wasVisible,
+            to = isVisible,
+            reason = loggingReason,
+        )
         return repository.setVisible(isVisible)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 1c87eb2..20ee393 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.model.SysUiState
 import com.android.systemui.model.updateFlags
 import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.logger.SceneLogger
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
@@ -57,13 +58,17 @@
     private val featureFlags: FeatureFlags,
     private val sysUiState: SysUiState,
     @DisplayId private val displayId: Int,
+    private val sceneLogger: SceneLogger,
 ) : CoreStartable {
 
     override fun start() {
         if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+            sceneLogger.logFrameworkEnabled(isEnabled = true)
             hydrateVisibility()
             automaticallySwitchScenes()
             hydrateSystemUiState()
+        } else {
+            sceneLogger.logFrameworkEnabled(isEnabled = false)
         }
     }
 
@@ -73,7 +78,9 @@
             sceneInteractor.currentScene
                 .map { it.key }
                 .distinctUntilChanged()
-                .collect { sceneKey -> sceneInteractor.setVisible(sceneKey != SceneKey.Gone) }
+                .collect { sceneKey ->
+                    sceneInteractor.setVisible(sceneKey != SceneKey.Gone, "scene is $sceneKey")
+                }
         }
     }
 
@@ -88,10 +95,17 @@
                         isUnlocked ->
                             when (currentSceneKey) {
                                 // When the device becomes unlocked in Bouncer, go to Gone.
-                                is SceneKey.Bouncer -> SceneKey.Gone
+                                is SceneKey.Bouncer ->
+                                    SceneKey.Gone to "device unlocked in Bouncer scene"
                                 // When the device becomes unlocked in Lockscreen, go to Gone if
                                 // bypass is enabled.
-                                is SceneKey.Lockscreen -> SceneKey.Gone.takeIf { isBypassEnabled }
+                                is SceneKey.Lockscreen ->
+                                    if (isBypassEnabled) {
+                                        SceneKey.Gone to
+                                            "device unlocked in Lockscreen scene with bypass"
+                                    } else {
+                                        null
+                                    }
                                 // We got unlocked while on a scene that's not Lockscreen or
                                 // Bouncer, no need to change scenes.
                                 else -> null
@@ -104,13 +118,19 @@
                                 is SceneKey.Bouncer -> null
                                 // We got locked while on a scene that's not Lockscreen or Bouncer,
                                 // go to Lockscreen.
-                                else -> SceneKey.Lockscreen
+                                else ->
+                                    SceneKey.Lockscreen to "device locked in $currentSceneKey scene"
                             }
                         else -> null
                     }
                 }
                 .filterNotNull()
-                .collect { targetSceneKey -> switchToScene(targetSceneKey) }
+                .collect { (targetSceneKey, loggingReason) ->
+                    switchToScene(
+                        targetSceneKey = targetSceneKey,
+                        loggingReason = loggingReason,
+                    )
+                }
         }
 
         applicationScope.launch {
@@ -121,7 +141,16 @@
                     if (isAsleep) {
                         // When the device goes to sleep, reset the current scene.
                         val isUnlocked = authenticationInteractor.isUnlocked.value
-                        switchToScene(if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen)
+                        val (targetSceneKey, loggingReason) =
+                            if (isUnlocked) {
+                                SceneKey.Gone to "device is asleep while unlocked"
+                            } else {
+                                SceneKey.Lockscreen to "device is asleep while locked"
+                            }
+                        switchToScene(
+                            targetSceneKey = targetSceneKey,
+                            loggingReason = loggingReason,
+                        )
                     }
                 }
         }
@@ -147,9 +176,10 @@
         }
     }
 
-    private fun switchToScene(targetSceneKey: SceneKey) {
+    private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
         sceneInteractor.setCurrentScene(
             scene = SceneModel(targetSceneKey),
+            loggingReason = loggingReason,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
new file mode 100644
index 0000000..0adbd5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 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 com.android.systemui.scene.shared.logger
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.SceneFrameworkLog
+import com.android.systemui.scene.shared.model.SceneKey
+import javax.inject.Inject
+
+class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: LogBuffer) {
+
+    fun logFrameworkEnabled(isEnabled: Boolean) {
+        fun asWord(isEnabled: Boolean): String {
+            return if (isEnabled) "enabled" else "disabled"
+        }
+
+        logBuffer.log(
+            tag = TAG,
+            level = LogLevel.INFO,
+            messageInitializer = { bool1 = isEnabled },
+            messagePrinter = { "Scene framework is ${asWord(bool1)}" }
+        )
+    }
+
+    fun logSceneChange(
+        from: SceneKey,
+        to: SceneKey,
+        reason: String,
+    ) {
+        logBuffer.log(
+            tag = TAG,
+            level = LogLevel.INFO,
+            messageInitializer = {
+                str1 = from.toString()
+                str2 = to.toString()
+                str3 = reason
+            },
+            messagePrinter = { "$str1 → $str2, reason: $str3" },
+        )
+    }
+
+    fun logVisibilityChange(
+        from: Boolean,
+        to: Boolean,
+        reason: String,
+    ) {
+        fun asWord(isVisible: Boolean): String {
+            return if (isVisible) "visible" else "invisible"
+        }
+
+        logBuffer.log(
+            tag = TAG,
+            level = LogLevel.INFO,
+            messageInitializer = {
+                str1 = asWord(from)
+                str2 = asWord(to)
+                str3 = reason
+            },
+            messagePrinter = { "$str1 → $str2, reason: $str3" },
+        )
+    }
+
+    companion object {
+        private const val TAG = "SceneFramework"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index bd73e36..b4ebaec 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -53,7 +53,10 @@
 
     /** Requests a transition to the scene with the given key. */
     fun setCurrentScene(scene: SceneModel) {
-        interactor.setCurrentScene(scene)
+        interactor.setCurrentScene(
+            scene = scene,
+            loggingReason = SCENE_TRANSITION_LOGGING_REASON,
+        )
     }
 
     /**
@@ -69,4 +72,8 @@
     fun onRemoteUserInput(event: MotionEvent) {
         interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
     }
+
+    companion object {
+        private const val SCENE_TRANSITION_LOGGING_REASON = "user input"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
new file mode 100644
index 0000000..8849d6e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
@@ -0,0 +1,29 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNotificationTestCases",
+      "options": [
+        {
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "CtsNotificationTestCases"
+    }
+  ]
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 1227287..2affb817 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -150,10 +150,10 @@
     fun onTouch(event: MotionEvent) {
         if (centralSurfaces.statusBarWindowState == WINDOW_STATE_SHOWING) {
             val upOrCancel =
-                    event.action == MotionEvent.ACTION_UP ||
+                event.action == MotionEvent.ACTION_UP ||
                     event.action == MotionEvent.ACTION_CANCEL
             centralSurfaces.setInteracting(WINDOW_STATUS_BAR,
-                    !upOrCancel || shadeController.isExpandedVisible)
+                !upOrCancel || shadeController.isExpandedVisible)
         }
     }
 
@@ -171,7 +171,7 @@
             if (!centralSurfaces.commandQueuePanelsEnabled) {
                 if (event.action == MotionEvent.ACTION_DOWN) {
                     Log.v(TAG, String.format("onTouchForwardedFromStatusBar: panel disabled, " +
-                            "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"))
+                        "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"))
                 }
                 return false
             }
@@ -182,7 +182,7 @@
                 sceneInteractor.get()
                     .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
                 // TODO(b/291965119): remove once view is expanded to cover the status bar
-                sceneInteractor.get().setVisible(true)
+                sceneInteractor.get().setVisible(true, "swipe down from status bar")
                 return false
             }
 
@@ -191,11 +191,11 @@
                 // bar eat the gesture.
                 if (!shadeViewController.isViewEnabled) {
                     shadeLogger.logMotionEvent(event,
-                            "onTouchForwardedFromStatusBar: panel view disabled")
+                        "onTouchForwardedFromStatusBar: panel view disabled")
                     return true
                 }
                 if (shadeViewController.isFullyCollapsed &&
-                        event.y < 1f) {
+                    event.y < 1f) {
                     // b/235889526 Eat events on the top edge of the phone when collapsed
                     shadeLogger.logMotionEvent(event, "top edge touch ignored")
                     return true
@@ -257,27 +257,27 @@
             view: PhoneStatusBarView
         ): PhoneStatusBarViewController {
             val statusBarMoveFromCenterAnimationController =
-                    if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
-                        unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
-                    } else {
-                        null
-                    }
+                if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
+                    unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
+                } else {
+                    null
+                }
 
             return PhoneStatusBarViewController(
-                    view,
-                    progressProvider.getOrNull(),
-                    centralSurfaces,
-                    shadeController,
-                    shadeViewController,
-                    sceneInteractor,
-                    shadeLogger,
-                    statusBarMoveFromCenterAnimationController,
-                    userChipViewModel,
-                    viewUtil,
-                    featureFlags,
-                    configurationController,
-                    statusOverlayHoverListenerFactory,
+                view,
+                progressProvider.getOrNull(),
+                centralSurfaces,
+                shadeController,
+                shadeViewController,
+                sceneInteractor,
+                shadeLogger,
+                statusBarMoveFromCenterAnimationController,
+                userChipViewModel,
+                viewUtil,
+                featureFlags,
+                configurationController,
+                statusOverlayHoverListenerFactory,
             )
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
index f4c5723..cffc833 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
@@ -38,6 +38,19 @@
 constructor(
     @VerboseMobileViewLog private val buffer: LogBuffer,
 ) {
+    fun logBinderReceivedVisibility(parentView: View, subId: Int, visibility: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = parentView.getIdForLogging()
+                int1 = subId
+                bool1 = visibility
+            },
+            { "Binder[subId=$int1, viewId=$str1] received visibility: $bool1" },
+        )
+    }
+
     fun logBinderReceivedSignalIcon(parentView: View, subId: Int, icon: SignalIconModel) {
         buffer.log(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index c221109..55bc8d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -99,7 +99,16 @@
                     }
                 }
 
-                launch { viewModel.isVisible.collect { isVisible -> view.isVisible = isVisible } }
+                launch {
+                    viewModel.isVisible.collect { isVisible ->
+                        viewModel.verboseLogger?.logBinderReceivedVisibility(
+                            view,
+                            viewModel.subscriptionId,
+                            isVisible
+                        )
+                        view.isVisible = isVisible
+                    }
+                }
 
                 // Set the icon for the triangle
                 launch {
diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
similarity index 68%
copy from packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
copy to packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
index e7738af..7e105cf 100644
--- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
@@ -13,29 +13,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.core.animation
+package android.animation
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import androidx.core.animation.doOnEnd
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.doOnEnd
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+/**
+ * This test class validates that two tests' animators are isolated from each other when using the
+ * same animator test rule. This is a test to prevent future instances of b/275602127.
+ */
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
-class AnimatorTestRuleTest : SysuiTestCase() {
+class AnimatorTestRuleIsolationTest : SysuiTestCase() {
 
     @get:Rule val animatorTestRule = AnimatorTestRule()
 
     @Test
     fun testA() {
+        // GIVEN global state is reset at the start of the test
         didTouchA = false
         didTouchB = false
+        // WHEN starting 2 animations of different durations, and setting didTouchA at the end
         ObjectAnimator.ofFloat(0f, 1f).apply {
             duration = 100
             doOnEnd { didTouchA = true }
@@ -46,15 +52,20 @@
             doOnEnd { didTouchA = true }
             start()
         }
+        // WHEN when you advance time so that only one of the animations has ended
         animatorTestRule.advanceTimeBy(100)
+        // VERIFY we did indeed end the current animation
         assertThat(didTouchA).isTrue()
+        // VERIFY advancing the animator did NOT cause testB's animator to end
         assertThat(didTouchB).isFalse()
     }
 
     @Test
     fun testB() {
+        // GIVEN global state is reset at the start of the test
         didTouchA = false
         didTouchB = false
+        // WHEN starting 2 animations of different durations, and setting didTouchB at the end
         ObjectAnimator.ofFloat(0f, 1f).apply {
             duration = 100
             doOnEnd { didTouchB = true }
@@ -66,7 +77,9 @@
             start()
         }
         animatorTestRule.advanceTimeBy(100)
+        // VERIFY advancing the animator did NOT cause testA's animator to end
         assertThat(didTouchA).isFalse()
+        // VERIFY we did indeed end the current animation
         assertThat(didTouchB).isTrue()
     }
 
diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
new file mode 100644
index 0000000..6c40368
--- /dev/null
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2023 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 android.animation
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.core.animation.doOnEnd
+import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+class AnimatorTestRulePrecisionTest : SysuiTestCase() {
+
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
+    var value1: Float = -1f
+    var value2: Float = -1f
+
+    private inline fun animateThis(
+        propertyName: String,
+        duration: Long,
+        startDelay: Long = 0,
+        crossinline onEndAction: (animator: Animator) -> Unit,
+    ) {
+        ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also {
+            it.interpolator = Interpolators.LINEAR
+            it.duration = duration
+            it.startDelay = startDelay
+            it.doOnEnd(onEndAction)
+            it.start()
+        }
+    }
+
+    @Test
+    fun testSingleAnimator() {
+        var ended = false
+        animateThis("value1", duration = 100) { ended = true }
+
+        assertThat(value1).isEqualTo(0f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(50)
+        assertThat(value1).isEqualTo(0.5f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(49)
+        assertThat(value1).isEqualTo(0.99f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(ended).isTrue()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+    }
+
+    @Test
+    fun testDelayedAnimator() {
+        var ended = false
+        animateThis("value1", duration = 100, startDelay = 50) { ended = true }
+
+        assertThat(value1).isEqualTo(-1f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(49)
+        assertThat(value1).isEqualTo(-1f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(0f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(99)
+        assertThat(value1).isEqualTo(0.99f)
+        assertThat(ended).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(ended).isTrue()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+    }
+
+    @Test
+    fun testTwoAnimators() {
+        var ended1 = false
+        var ended2 = false
+        animateThis("value1", duration = 100) { ended1 = true }
+        animateThis("value2", duration = 200) { ended2 = true }
+        assertThat(value1).isEqualTo(0f)
+        assertThat(value2).isEqualTo(0f)
+        assertThat(ended1).isFalse()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
+
+        animatorTestRule.advanceTimeBy(99)
+        assertThat(value1).isEqualTo(0.99f)
+        assertThat(value2).isEqualTo(0.495f)
+        assertThat(ended1).isFalse()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(0.5f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(99)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(0.995f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(1f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isTrue()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+    }
+
+    @Test
+    fun testChainedAnimators() {
+        var ended1 = false
+        var ended2 = false
+        animateThis("value1", duration = 100) {
+            ended1 = true
+            animateThis("value2", duration = 100) { ended2 = true }
+        }
+
+        assertThat(value1).isEqualTo(0f)
+        assertThat(value2).isEqualTo(-1f)
+        assertThat(ended1).isFalse()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(99)
+        assertThat(value1).isEqualTo(0.99f)
+        assertThat(value2).isEqualTo(-1f)
+        assertThat(ended1).isFalse()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(0f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(99)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(0.99f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isFalse()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+        animatorTestRule.advanceTimeBy(1)
+        assertThat(value1).isEqualTo(1f)
+        assertThat(value2).isEqualTo(1f)
+        assertThat(ended1).isTrue()
+        assertThat(ended2).isTrue()
+        assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
similarity index 70%
rename from packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
rename to packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
index e7738af..d034093 100644
--- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
+++ b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
@@ -25,17 +25,23 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+/**
+ * This test class validates that two tests' animators are isolated from each other when using the
+ * same animator test rule. This is a test to prevent future instances of b/275602127.
+ */
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
-class AnimatorTestRuleTest : SysuiTestCase() {
+class AnimatorTestRuleIsolationTest : SysuiTestCase() {
 
     @get:Rule val animatorTestRule = AnimatorTestRule()
 
     @Test
     fun testA() {
+        // GIVEN global state is reset at the start of the test
         didTouchA = false
         didTouchB = false
+        // WHEN starting 2 animations of different durations, and setting didTouchA at the end
         ObjectAnimator.ofFloat(0f, 1f).apply {
             duration = 100
             doOnEnd { didTouchA = true }
@@ -46,15 +52,20 @@
             doOnEnd { didTouchA = true }
             start()
         }
+        // WHEN when you advance time so that only one of the animations has ended
         animatorTestRule.advanceTimeBy(100)
+        // VERIFY we did indeed end the current animation
         assertThat(didTouchA).isTrue()
+        // VERIFY advancing the animator did NOT cause testB's animator to end
         assertThat(didTouchB).isFalse()
     }
 
     @Test
     fun testB() {
+        // GIVEN global state is reset at the start of the test
         didTouchA = false
         didTouchB = false
+        // WHEN starting 2 animations of different durations, and setting didTouchB at the end
         ObjectAnimator.ofFloat(0f, 1f).apply {
             duration = 100
             doOnEnd { didTouchB = true }
@@ -66,7 +77,9 @@
             start()
         }
         animatorTestRule.advanceTimeBy(100)
+        // VERIFY advancing the animator did NOT cause testA's animator to end
         assertThat(didTouchA).isFalse()
+        // VERIFY we did indeed end the current animation
         assertThat(didTouchB).isTrue()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index e447c29..efb981e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -733,20 +733,20 @@
             // is
             // not enough to trigger a dismissal of the keyguard.
             underTest.onViewAttached()
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
             runCurrent()
             verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
 
             // While listening, going from the bouncer scene to the gone scene, does dismiss the
             // keyguard.
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
             runCurrent()
             verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt())
 
             // While listening, moving back to the bouncer scene does not dismiss the keyguard
             // again.
             clearInvocations(viewMediatorCallback)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
             runCurrent()
             verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
 
@@ -754,12 +754,12 @@
             // scene
             // does not dismiss the keyguard while we're not listening.
             underTest.onViewDetached()
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
             runCurrent()
             verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
 
             // While not listening, moving back to the bouncer does not dismiss the keyguard.
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
             runCurrent()
             verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
 
@@ -767,7 +767,7 @@
             // gone
             // scene now does dismiss the keyguard again.
             underTest.onViewAttached()
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
             runCurrent()
             verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt())
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
index 992ee1a..efae3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
@@ -59,63 +59,79 @@
     }
 
     @Test
-    fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+    fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAuthIsAllowed() =
         testScope.runTest {
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAuthAllowed = false)
                 .isEqualTo("Enter PIN")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAuthAllowed = true)
                 .isEqualTo("Unlock with PIN or fingerprint")
 
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAuthAllowed = false)
                 .isEqualTo("Enter password")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAuthAllowed = true)
                 .isEqualTo("Unlock with password or fingerprint")
 
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAuthAllowed = false)
                 .isEqualTo("Draw pattern")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true)
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAuthAllowed = true)
                 .isEqualTo("Unlock with pattern or fingerprint")
         }
 
     @Test
-    fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+    fun bouncerMessages_overridesSecondaryMessageValue() =
+        testScope.runTest {
+            val bouncerMessageModel =
+                bouncerMessageModel(
+                    PIN,
+                    true,
+                    PROMPT_REASON_DEFAULT,
+                    secondaryMessageOverride = "face acquisition message"
+                )!!
+            assertThat(context.resources.getString(bouncerMessageModel.message!!.messageResId!!))
+                .isEqualTo("Unlock with PIN or fingerprint")
+            assertThat(bouncerMessageModel.secondaryMessage!!.message!!)
+                .isEqualTo("face acquisition message")
+        }
+
+    @Test
+    fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAuthIsAllowed() =
         testScope.runTest {
             primaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = PIN,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Wrong PIN. Try again.")
             secondaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = PIN,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Or unlock with fingerprint")
 
             primaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = Password,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Wrong password. Try again.")
             secondaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = Password,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Or unlock with fingerprint")
 
             primaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = Pattern,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Wrong pattern. Try again.")
             secondaryMessage(
                     PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
                     mode = Pattern,
-                    fpAllowedInBouncer = true
+                    fpAuthAllowed = true
                 )
                 .isEqualTo("Or unlock with fingerprint")
         }
@@ -123,11 +139,11 @@
     private fun primaryMessage(
         reason: Int,
         mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean
+        fpAuthAllowed: Boolean
     ): StringSubject {
         return assertThat(
             context.resources.getString(
-                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!!
+                bouncerMessageModel(mode, fpAuthAllowed, reason)!!.message!!.messageResId!!
             )
         )!!
     }
@@ -135,25 +151,28 @@
     private fun secondaryMessage(
         reason: Int,
         mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean
+        fpAuthAllowed: Boolean
     ): StringSubject {
         return assertThat(
             context.resources.getString(
-                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!
-                    .secondaryMessage!!
-                    .messageResId!!
+                bouncerMessageModel(mode, fpAuthAllowed, reason)!!.secondaryMessage!!.messageResId!!
             )
         )!!
     }
 
     private fun bouncerMessageModel(
         mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean,
-        reason: Int
+        fpAuthAllowed: Boolean,
+        reason: Int,
+        secondaryMessageOverride: String? = null,
     ): BouncerMessageModel? {
         whenever(securityModel.getSecurityMode(0)).thenReturn(mode)
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer)
+        whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(fpAuthAllowed)
 
-        return underTest.createFromPromptReason(reason, 0)
+        return underTest.createFromPromptReason(
+            reason,
+            0,
+            secondaryMsgOverride = secondaryMessageOverride
+        )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
index de712da..2be7d8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
@@ -51,6 +51,7 @@
 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
 import com.android.systemui.bouncer.shared.model.Message
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
@@ -79,6 +80,7 @@
 
     @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var securityModel: KeyguardSecurityModel
+    @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
     @Captor
     private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
 
@@ -99,7 +101,7 @@
         fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
         testScope = TestScope()
 
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+        whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false)
         whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
         underTest =
             BouncerMessageRepositoryImpl(
@@ -108,7 +110,8 @@
                 updateMonitor = updateMonitor,
                 bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
                 userRepository = userRepository,
-                fingerprintAuthRepository = fingerprintRepository
+                fingerprintAuthRepository = fingerprintRepository,
+                systemPropertiesHelper = systemPropertiesHelper
             )
     }
 
@@ -214,6 +217,21 @@
         }
 
     @Test
+    fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            whenever(systemPropertiesHelper.get("sys.boot.reason.last"))
+                .thenReturn("reboot,mainline_update")
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            biometricSettingsRepository.setFaceEnrolled(true)
+            biometricSettingsRepository.setIsFaceAuthEnabled(true)
+
+            verifyMessagesForAuthFlag(
+                STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair(keyguard_enter_pin, R.string.kg_prompt_after_update_pin),
+            )
+        }
+
+    @Test
     fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
         testScope.runTest {
             userRepository.setSelectedUserInfo(PRIMARY_USER)
@@ -345,8 +363,8 @@
 
     private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
         return BouncerMessageModel(
-            message = Message(messageResId = primaryResId),
-            secondaryMessage = Message(messageResId = secondaryResId)
+            message = Message(messageResId = primaryResId, animate = false),
+            secondaryMessage = Message(messageResId = secondaryResId, animate = false)
         )
     }
     private fun message(value: String): BouncerMessageModel {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 8e5256e..3ca94aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -76,7 +76,7 @@
 
         allowTestableLooperAsMainThread()
         whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+        whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false)
     }
 
     suspend fun TestScope.init() {
@@ -150,11 +150,12 @@
 
             underTest.setCustomMessage("not empty")
 
-            assertThat(repository.customMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+            val customMessage = repository.customMessage
+            assertThat(customMessage.value!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
+            assertThat(customMessage.value!!.secondaryMessage!!.message).isEqualTo("not empty")
 
             underTest.setCustomMessage(null)
-            assertThat(repository.customMessage.value).isNull()
+            assertThat(customMessage.value).isNull()
         }
 
     @Test
@@ -164,11 +165,15 @@
 
             underTest.setFaceAcquisitionMessage("not empty")
 
-            assertThat(repository.faceAcquisitionMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+            val faceAcquisitionMessage = repository.faceAcquisitionMessage
+
+            assertThat(faceAcquisitionMessage.value!!.message!!.messageResId)
+                .isEqualTo(keyguard_enter_pin)
+            assertThat(faceAcquisitionMessage.value!!.secondaryMessage!!.message)
+                .isEqualTo("not empty")
 
             underTest.setFaceAcquisitionMessage(null)
-            assertThat(repository.faceAcquisitionMessage.value).isNull()
+            assertThat(faceAcquisitionMessage.value).isNull()
         }
 
     @Test
@@ -178,11 +183,15 @@
 
             underTest.setFingerprintAcquisitionMessage("not empty")
 
-            assertThat(repository.fingerprintAcquisitionMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+            val fingerprintAcquisitionMessage = repository.fingerprintAcquisitionMessage
+
+            assertThat(fingerprintAcquisitionMessage.value!!.message!!.messageResId)
+                .isEqualTo(keyguard_enter_pin)
+            assertThat(fingerprintAcquisitionMessage.value!!.secondaryMessage!!.message)
+                .isEqualTo("not empty")
 
             underTest.setFingerprintAcquisitionMessage(null)
-            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+            assertThat(fingerprintAcquisitionMessage.value).isNull()
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 1f089ca..7f8d54c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -79,7 +79,7 @@
                 AuthenticationMethodModel.Password
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
@@ -99,7 +99,7 @@
                 AuthenticationMethodModel.Password
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             runCurrent()
@@ -119,7 +119,7 @@
                 AuthenticationMethodModel.Password
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("password")
@@ -139,7 +139,7 @@
                 AuthenticationMethodModel.Password
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("wrong")
@@ -161,7 +161,7 @@
                 AuthenticationMethodModel.Password
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("wrong")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index af54989..57fcbe5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -83,7 +83,7 @@
                 AuthenticationMethodModel.Pattern
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
@@ -105,7 +105,7 @@
                 AuthenticationMethodModel.Pattern
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             runCurrent()
@@ -128,7 +128,7 @@
                 AuthenticationMethodModel.Pattern
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
@@ -176,7 +176,7 @@
                 AuthenticationMethodModel.Pattern
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
@@ -208,7 +208,7 @@
                 AuthenticationMethodModel.Pattern
             )
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index c12ed03..81c68ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -79,7 +79,7 @@
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
@@ -97,7 +97,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             runCurrent()
@@ -117,7 +117,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             runCurrent()
@@ -138,7 +138,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
 
@@ -161,7 +161,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             runCurrent()
@@ -183,7 +183,7 @@
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -203,7 +203,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPinButtonClicked(1)
@@ -227,7 +227,7 @@
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPinButtonClicked(1)
@@ -258,7 +258,7 @@
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
             utils.authenticationRepository.setAutoConfirmEnabled(true)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -277,7 +277,7 @@
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             utils.authenticationRepository.setUnlocked(false)
             utils.authenticationRepository.setAutoConfirmEnabled(true)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
index d825c2a..86e56bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
@@ -130,11 +130,11 @@
     fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
         testScope.runTest {
             val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Lockscreen))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Lockscreen), "reason")
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(isUnlocked).isFalse()
 
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
 
             assertThat(isUnlocked).isFalse()
         }
@@ -144,13 +144,13 @@
         testScope.runTest {
             val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             runCurrent()
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
             runCurrent()
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
             runCurrent()
             assertThat(isUnlocked).isFalse()
 
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
 
             assertThat(isUnlocked).isFalse()
         }
@@ -161,7 +161,7 @@
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
             runCurrent()
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.QuickSettings))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.QuickSettings), "reason")
             runCurrent()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index c193d83..4facc7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -52,7 +52,7 @@
         val currentScene by collectLastValue(underTest.currentScene)
         assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
 
-        underTest.setCurrentScene(SceneModel(SceneKey.Shade))
+        underTest.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
         assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
     }
 
@@ -79,10 +79,10 @@
         val isVisible by collectLastValue(underTest.isVisible)
         assertThat(isVisible).isTrue()
 
-        underTest.setVisible(false)
+        underTest.setVisible(false, "reason")
         assertThat(isVisible).isFalse()
 
-        underTest.setVisible(true)
+        underTest.setVisible(true, "reason")
         assertThat(isVisible).isTrue()
     }
 
@@ -92,7 +92,7 @@
         assertThat(transitions).isNull()
 
         val initialSceneKey = underTest.currentScene.value.key
-        underTest.setCurrentScene(SceneModel(SceneKey.Shade))
+        underTest.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
         assertThat(transitions)
             .isEqualTo(
                 SceneTransitionModel(
@@ -101,7 +101,7 @@
                 )
             )
 
-        underTest.setCurrentScene(SceneModel(SceneKey.QuickSettings))
+        underTest.setCurrentScene(SceneModel(SceneKey.QuickSettings), "reason")
         assertThat(transitions)
             .isEqualTo(
                 SceneTransitionModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index b6bd31f..6be19b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -73,6 +73,7 @@
             featureFlags = featureFlags,
             sysUiState = sysUiState,
             displayId = Display.DEFAULT_DISPLAY,
+            sceneLogger = mock(),
         )
 
     @Before
@@ -97,7 +98,7 @@
 
             assertThat(isVisible).isFalse()
 
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
             assertThat(isVisible).isTrue()
         }
 
@@ -117,10 +118,10 @@
             underTest.start()
             assertThat(isVisible).isTrue()
 
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
             assertThat(isVisible).isTrue()
 
-            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+            sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
             assertThat(isVisible).isTrue()
         }
 
@@ -326,7 +327,7 @@
                     SceneKey.QuickSettings,
                 )
                 .forEachIndexed { index, sceneKey ->
-                    sceneInteractor.setCurrentScene(SceneModel(sceneKey))
+                    sceneInteractor.setCurrentScene(SceneModel(sceneKey), "reason")
                     runCurrent()
 
                     verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
@@ -342,7 +343,7 @@
         featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled)
         authenticationRepository.setUnlocked(isDeviceUnlocked)
         keyguardRepository.setBypassEnabled(isBypassEnabled)
-        initialSceneKey?.let { sceneInteractor.setCurrentScene(SceneModel(it)) }
+        initialSceneKey?.let { sceneInteractor.setCurrentScene(SceneModel(it), "reason") }
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 0ab98ad..9f3b12b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -52,10 +52,10 @@
         val isVisible by collectLastValue(underTest.isVisible)
         assertThat(isVisible).isTrue()
 
-        interactor.setVisible(false)
+        interactor.setVisible(false, "reason")
         assertThat(isVisible).isFalse()
 
-        interactor.setVisible(true)
+        interactor.setVisible(true, "reason")
         assertThat(isVisible).isTrue()
     }
 
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt b/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt
new file mode 100644
index 0000000..b74ddae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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 android.animation
+
+import android.os.Looper
+import android.util.Log
+import com.android.systemui.util.test.TestExceptionDeferrer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * This rule is intended to be used by System UI tests that are otherwise blocked from using
+ * animators because of [PlatformAnimatorIsolationRule]. It is preferred that test authors use
+ * [AnimatorTestRule], as that rule allows test authors to step through animations and removes the
+ * need for tests to handle multiple threads. However, many System UI tests were written before this
+ * was conceivable, so this rule is intended to support those legacy tests.
+ */
+class AnimatorIsolationWorkaroundRule(
+    private val requiredLooper: Looper? = Looper.getMainLooper(),
+) : TestRule {
+    private inner class IsolationWorkaroundHandler(ruleThread: Thread) : AnimationHandler() {
+        private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+        private val addedCallbacks = mutableSetOf<AnimationFrameCallback>()
+
+        fun tearDownAndThrowDeferred() {
+            addedCallbacks.forEach { super.removeCallback(it) }
+            exceptionDeferrer.throwDeferred()
+        }
+
+        override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) {
+            checkLooper()
+            if (callback != null) {
+                addedCallbacks.add(callback)
+            }
+            super.addAnimationFrameCallback(callback, delay)
+        }
+
+        override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) {
+            checkLooper()
+            super.addOneShotCommitCallback(callback)
+        }
+
+        override fun removeCallback(callback: AnimationFrameCallback?) {
+            super.removeCallback(callback)
+        }
+
+        override fun setProvider(provider: AnimationFrameCallbackProvider?) {
+            checkLooper()
+            super.setProvider(provider)
+        }
+
+        override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) {
+            checkLooper()
+            super.autoCancelBasedOn(objectAnimator)
+        }
+
+        private fun checkLooper() {
+            exceptionDeferrer.check(requiredLooper == null || Looper.myLooper() == requiredLooper) {
+                "Animations are being registered on a different looper than the expected one!" +
+                    " expected=$requiredLooper actual=${Looper.myLooper()}"
+            }
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                val workaroundHandler = IsolationWorkaroundHandler(Thread.currentThread())
+                val prevInstance = AnimationHandler.setTestHandler(workaroundHandler)
+                check(PlatformAnimatorIsolationRule.isIsolatingHandler(prevInstance)) {
+                    "AnimatorIsolationWorkaroundRule must be used within " +
+                        "PlatformAnimatorIsolationRule, but test handler was $prevInstance"
+                }
+                try {
+                    base.evaluate()
+                    val count = AnimationHandler.getAnimationCount()
+                    if (count > 0) {
+                        Log.w(TAG, "Animations still running: $count")
+                    }
+                } finally {
+                    val handlerAtEnd = AnimationHandler.setTestHandler(prevInstance)
+                    check(workaroundHandler == handlerAtEnd) {
+                        "Test handler was altered: expected=$workaroundHandler actual=$handlerAtEnd"
+                    }
+                    // Pass or fail, errors caught here should be the reason the test fails
+                    workaroundHandler.tearDownAndThrowDeferred()
+                }
+            }
+        }
+    }
+
+    private companion object {
+        private const val TAG = "AnimatorIsolationWorkaroundRule"
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
new file mode 100644
index 0000000..6535f33
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 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 android.animation;
+
+import android.animation.AnimationHandler.AnimationFrameCallback;
+import android.annotation.NonNull;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.AndroidRuntimeException;
+import android.view.Choreographer;
+
+import com.android.internal.util.Preconditions;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the
+ * duration of the animation. This also helps the test to be written in a deterministic manner.
+ *
+ * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule}
+ * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started.
+ * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to
+ * start the animator.
+ *
+ * <pre>
+ * {@literal @}SmallTest
+ * {@literal @}RunWith(AndroidJUnit4.class)
+ * public class SampleAnimatorTest {
+ *
+ *     {@literal @}Rule
+ *     public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
+ *
+ *     {@literal @}UiThreadTest
+ *     {@literal @}Test
+ *     public void sample() {
+ *         final ValueAnimator animator = ValueAnimator.ofInt(0, 1000);
+ *         animator.setDuration(1000L);
+ *         assertThat(animator.getAnimatedValue(), is(0));
+ *         animator.start();
+ *         sAnimatorTestRule.advanceTimeBy(500L);
+ *         assertThat(animator.getAnimatedValue(), is(500));
+ *     }
+ * }
+ * </pre>
+ */
+public final class AnimatorTestRule implements TestRule {
+
+    private final Object mLock = new Object();
+    private final TestHandler mTestHandler = new TestHandler();
+    /**
+     * initializing the start time with {@link SystemClock#uptimeMillis()} reduces the discrepancies
+     * with various internals of classes like ValueAnimator which can sometimes read that clock via
+     * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+     */
+    private final long mStartTime = SystemClock.uptimeMillis();
+    private long mTotalTimeDelta = 0;
+
+    @NonNull
+    @Override
+    public Statement apply(@NonNull final Statement base, @NonNull Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler);
+                try {
+                    base.evaluate();
+                } finally {
+                    AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
+                    if (mTestHandler != objAtEnd) {
+                        // pass or fail, inner logic not restoring the handler needs to be reported.
+                        // noinspection ThrowFromFinallyBlock
+                        throw new IllegalStateException("Test handler was altered: expected="
+                                + mTestHandler + " actual=" + objAtEnd);
+                    }
+                }
+            }
+        };
+    }
+
+    /**
+     * If any new {@link Animator}s have been registered since the last time the frame time was
+     * advanced, initialize them with the current frame time.  Failing to do this will result in the
+     * animations beginning on the *next* advancement instead, so this is done automatically for
+     * test authors inside of {@link #advanceTimeBy}.  However this is exposed in case authors want
+     * to validate operations performed by onStart listeners.
+     * <p>
+     * NOTE: This is only required of the platform ValueAnimator because its start() method calls
+     * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this
+     * rule can't synchronously trigger the callback at that time.
+     */
+    public void initNewAnimators() {
+        requireLooper("AnimationTestRule#initNewAnimators()");
+        long currentTime = getCurrentTime();
+        List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks);
+        mTestHandler.mNewCallbacks.clear();
+        for (AnimationFrameCallback newCallback : newCallbacks) {
+            newCallback.doAnimationFrame(currentTime);
+        }
+    }
+
+    /**
+     * Advances the animation clock by the given amount of delta in milliseconds. This call will
+     * produce an animation frame to all the ongoing animations. This method needs to be
+     * called on the same thread as {@link Animator#start()}.
+     *
+     * @param timeDelta the amount of milliseconds to advance
+     */
+    public void advanceTimeBy(long timeDelta) {
+        Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
+        requireLooper("AnimationTestRule#advanceTimeBy(long)");
+        // before advancing time, start new animators with the current time
+        initNewAnimators();
+        synchronized (mLock) {
+            // advance time
+            mTotalTimeDelta += timeDelta;
+        }
+        // produce a frame
+        mTestHandler.doFrame();
+    }
+
+    /**
+     * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a
+     * different time than the time tracked by {@link SystemClock} This method needs to be called on
+     * the same thread as {@link Animator#start()}.
+     */
+    public long getCurrentTime() {
+        requireLooper("AnimationTestRule#getCurrentTime()");
+        synchronized (mLock) {
+            return mStartTime + mTotalTimeDelta;
+        }
+    }
+
+    private static void requireLooper(String method) {
+        if (Looper.myLooper() == null) {
+            throw new AndroidRuntimeException(method + " may only be called on Looper threads");
+        }
+    }
+
+    private class TestHandler extends AnimationHandler {
+        public final TestProvider mTestProvider = new TestProvider();
+        private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>();
+
+        TestHandler() {
+            setProvider(mTestProvider);
+        }
+
+        public void doFrame() {
+            mTestProvider.animateFrame();
+            mTestProvider.commitFrame();
+        }
+
+        @Override
+        public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) {
+            // NOTE: using the delay is infeasible because the AnimationHandler uses
+            //  SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we
+            //  could fix this for tests.
+            super.addAnimationFrameCallback(callback, 0);
+            if (delay <= 0) {
+                mNewCallbacks.add(callback);
+            }
+        }
+
+        @Override
+        public void removeCallback(AnimationFrameCallback callback) {
+            super.removeCallback(callback);
+            mNewCallbacks.remove(callback);
+        }
+    }
+
+    private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider {
+        private long mFrameDelay = 10;
+        private Choreographer.FrameCallback mFrameCallback = null;
+        private final List<Runnable> mCommitCallbacks = new ArrayList<>();
+
+        public void animateFrame() {
+            Choreographer.FrameCallback frameCallback = mFrameCallback;
+            mFrameCallback = null;
+            if (frameCallback != null) {
+                frameCallback.doFrame(getFrameTime());
+            }
+        }
+
+        public void commitFrame() {
+            List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks);
+            mCommitCallbacks.clear();
+            for (Runnable commitCallback : commitCallbacks) {
+                commitCallback.run();
+            }
+        }
+
+        @Override
+        public void postFrameCallback(Choreographer.FrameCallback callback) {
+            assert mFrameCallback == null;
+            mFrameCallback = callback;
+        }
+
+        @Override
+        public void postCommitCallback(Runnable runnable) {
+            mCommitCallbacks.add(runnable);
+        }
+
+        @Override
+        public void setFrameDelay(long delay) {
+            mFrameDelay = delay;
+        }
+
+        @Override
+        public long getFrameDelay() {
+            return mFrameDelay;
+        }
+
+        @Override
+        public long getFrameTime() {
+            return getCurrentTime();
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt b/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt
new file mode 100644
index 0000000..43a26f3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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 android.animation
+
+import com.android.systemui.util.test.TestExceptionDeferrer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
+ * start a Platform [Animator] without using [android.animation.AnimatorTestRule].
+ *
+ * TODO(b/291645410): enable this; currently this causes hundreds of test failures.
+ */
+class PlatformAnimatorIsolationRule : TestRule {
+
+    private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler() {
+        private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+        override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) = onError()
+        override fun removeCallback(callback: AnimationFrameCallback?) = onError()
+        override fun setProvider(provider: AnimationFrameCallbackProvider?) = onError()
+        override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) = onError()
+        override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) =
+            onError()
+
+        private fun onError() =
+            exceptionDeferrer.fail(
+                "Test's animations are not isolated! " +
+                    "Did you forget to add an AnimatorTestRule to your test class?"
+            )
+
+        fun throwDeferred() = exceptionDeferrer.throwDeferred()
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
+                val originalHandler = AnimationHandler.setTestHandler(isolationHandler)
+                try {
+                    base.evaluate()
+                } finally {
+                    val handlerAtEnd = AnimationHandler.setTestHandler(originalHandler)
+                    check(isolationHandler == handlerAtEnd) {
+                        "Test handler was altered: expected=$isolationHandler actual=$handlerAtEnd"
+                    }
+                    // Pass or fail, a deferred exception should be the failure reason
+                    isolationHandler.throwDeferred()
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "PlatformAnimatorIsolationRule"
+
+        fun isIsolatingHandler(handler: AnimationHandler?): Boolean =
+            handler is IsolatingAnimationHandler
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt b/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
index 026372f..7a97029 100644
--- a/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
+++ b/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
@@ -16,40 +16,51 @@
 
 package androidx.core.animation
 
+import com.android.systemui.util.test.TestExceptionDeferrer
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
+/**
+ * This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
+ * start an AndroidX [Animator] without using [androidx.core.animation.AnimatorTestRule].
+ */
 class AndroidXAnimatorIsolationRule : TestRule {
 
-    private class TestAnimationHandler : AnimationHandler(null) {
-        override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = doFail()
-        override fun removeCallback(callback: AnimationFrameCallback?) = doFail()
-        override fun onAnimationFrame(frameTime: Long) = doFail()
-        override fun setFrameDelay(frameDelay: Long) = doFail()
-        override fun getFrameDelay(): Long = doFail()
+    private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler(null) {
+        private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+        override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = onError()
+        override fun removeCallback(callback: AnimationFrameCallback?) = onError()
+        override fun onAnimationFrame(frameTime: Long) = onError()
+        override fun setFrameDelay(frameDelay: Long) = onError()
+
+        private fun onError() =
+            exceptionDeferrer.fail(
+                "Test's animations are not isolated! " +
+                    "Did you forget to add an AnimatorTestRule to your test class?"
+            )
+
+        fun throwDeferred() = exceptionDeferrer.throwDeferred()
     }
 
     override fun apply(base: Statement, description: Description): Statement {
         return object : Statement() {
             @Throws(Throwable::class)
             override fun evaluate() {
-                AnimationHandler.setTestHandler(testHandler)
+                val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
+                AnimationHandler.setTestHandler(isolationHandler)
                 try {
                     base.evaluate()
                 } finally {
                     AnimationHandler.setTestHandler(null)
+                    // Pass or fail, a deferred exception should be the failure reason
+                    isolationHandler.throwDeferred()
                 }
             }
         }
     }
 
-    companion object {
-        private val testHandler = TestAnimationHandler()
-        private fun doFail(): Nothing =
-            error(
-                "Test's animations are not isolated! " +
-                    "Did you forget to add an AnimatorTestRule to your test class?"
-            )
+    private companion object {
+        private const val TAG = "AndroidXAnimatorIsolationRule"
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 70d15a0..6cffb66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -92,6 +92,7 @@
             )
         }
     }
+
     private val context = test.context
 
     fun fakeSceneContainerRepository(
@@ -124,6 +125,7 @@
     ): SceneInteractor {
         return SceneInteractor(
             repository = repository,
+            logger = mock(),
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt
new file mode 100644
index 0000000..90281ca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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 com.android.systemui.util.test
+
+import android.util.Log
+
+/**
+ * Helper class that intercepts test errors which may be occurring on the wrong thread, and saves
+ * them so that they can be rethrown back on the correct thread.
+ */
+class TestExceptionDeferrer(private val tag: String, private val testThread: Thread) {
+    private val deferredErrors = mutableListOf<IllegalStateException>()
+
+    /** Ensure the [value] is `true`; otherwise [fail] with the produced [message] */
+    fun check(value: Boolean, message: () -> Any?) {
+        if (value) return
+        fail(message().toString())
+    }
+
+    /**
+     * If the [Thread.currentThread] is the [testThread], then [error], otherwise [Log] and defer
+     * the error until [throwDeferred] is called.
+     */
+    fun fail(message: String) {
+        if (testThread == Thread.currentThread()) {
+            error(message)
+        } else {
+            val exception = IllegalStateException(message)
+            Log.e(tag, "Deferring error: ", exception)
+            deferredErrors.add(exception)
+        }
+    }
+
+    /** If any [fail] or failed [check] has happened, throw the first one. */
+    fun throwDeferred() {
+        deferredErrors.firstOrNull()?.let { firstError ->
+            Log.e(tag, "Deferred errors: ${deferredErrors.size}")
+            deferredErrors.clear()
+            throw firstError
+        }
+    }
+}