Glitchy notifs on clock size change

Notifications weren't animating Y position changes as expected. In order
to synchronize the animation, we tether the intrablueprint transition
state into KeyguardRootViewBinder which listens for placeholder position
changes and forwards them to the notification code. This function now
skips certain layouts during transitions to prevent later layout changes
from conflicting with the running animation. These layouts are erronous
to the transition and suppressed by the transition directly for other
relevant lockscreen elements.

To facilitate two binders relying on the same transition state, I've
moved the transition management logic from the binder to the model, but
otherwise not modified it much. This makes the state available to more
components, allows the binder to be stateless, and lets us convert it
to being a top-level kotlin object like other binders.

I've also corrected an issue where the notif shelf would animate in when
the clock size changed. AodNotificationIconsSection.applyConstraints was
not setting the visibility, so it'd default to VISIBLE for a moment when
it was applied. The transition would pick up this incorrect transient
visibility value and animate the element in.

Bug: 341932557
Test: Manual & Presubmits
Flag: com.android.systemui.migrate_clocks_to_blueprint
Change-Id: Ib447caeb142f55a04f77f34cf8bd7fe6e7e83896
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index a50cc8f..306f4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -103,7 +103,6 @@
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     private val lockscreenContentViewModel: LockscreenContentViewModel,
     private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
-    private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder,
     private val clockInteractor: KeyguardClockInteractor,
     private val keyguardViewMediator: KeyguardViewMediator,
 ) : CoreStartable {
@@ -150,7 +149,7 @@
                 cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM)
                 keyguardRootView.addView(composeView)
             } else {
-                keyguardBlueprintViewBinder.bind(
+                KeyguardBlueprintViewBinder.bind(
                     keyguardRootView,
                     keyguardBlueprintViewModel,
                     keyguardClockViewModel,
@@ -197,12 +196,14 @@
             KeyguardRootViewBinder.bind(
                 keyguardRootView,
                 keyguardRootViewModel,
+                keyguardBlueprintViewModel,
                 configuration,
                 occludingAppDeviceEntryMessageViewModel,
                 chipbarCoordinator,
                 screenOffAnimationController,
                 shadeInteractor,
                 clockInteractor,
+                keyguardClockViewModel,
                 interactionJankMonitor,
                 deviceEntryHapticsInteractor,
                 vibratorHelper,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 8160335..bec8f3d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -17,17 +17,12 @@
 
 package com.android.systemui.keyguard.ui.binder
 
-import android.os.Handler
-import android.transition.Transition
-import android.transition.TransitionManager
 import android.util.Log
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
@@ -40,47 +35,9 @@
 import com.android.systemui.res.R
 import com.android.systemui.shared.R as sharedR
 import com.android.systemui.util.kotlin.pairwise
-import javax.inject.Inject
-import kotlin.math.max
 
-@SysUISingleton
-class KeyguardBlueprintViewBinder
-@Inject
-constructor(
-    @Main private val handler: Handler,
-) {
-    private var runningPriority = -1
-    private val runningTransitions = mutableSetOf<Transition>()
-    private val isTransitionRunning: Boolean
-        get() = runningTransitions.size > 0
-    private val transitionListener =
-        object : Transition.TransitionListener {
-            override fun onTransitionCancel(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
-                runningTransitions.remove(transition)
-            }
-
-            override fun onTransitionEnd(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
-                runningTransitions.remove(transition)
-            }
-
-            override fun onTransitionPause(transition: Transition) {
-                if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}")
-                runningTransitions.remove(transition)
-            }
-
-            override fun onTransitionResume(transition: Transition) {
-                if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}")
-                runningTransitions.add(transition)
-            }
-
-            override fun onTransitionStart(transition: Transition) {
-                if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}")
-                runningTransitions.add(transition)
-            }
-        }
-
+object KeyguardBlueprintViewBinder {
+    @JvmStatic
     fun bind(
         constraintLayout: ConstraintLayout,
         viewModel: KeyguardBlueprintViewModel,
@@ -118,7 +75,7 @@
                                     )
                                 }
 
-                            runTransition(constraintLayout, transition, config) {
+                            viewModel.runTransition(constraintLayout, transition, config) {
                                 // Replace sections from the previous blueprint with the new ones
                                 blueprint.replaceViews(
                                     constraintLayout,
@@ -146,7 +103,7 @@
                     viewModel.refreshTransition.collect { config ->
                         val blueprint = viewModel.blueprint.value
 
-                        runTransition(
+                        viewModel.runTransition(
                             constraintLayout,
                             IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel),
                             config,
@@ -167,50 +124,6 @@
         }
     }
 
-    private fun runTransition(
-        constraintLayout: ConstraintLayout,
-        transition: Transition,
-        config: Config,
-        apply: () -> Unit,
-    ) {
-        val currentPriority = if (isTransitionRunning) runningPriority else -1
-        if (config.checkPriority && config.type.priority < currentPriority) {
-            if (DEBUG) {
-                Log.w(
-                    TAG,
-                    "runTransition: skipping ${transition::class.simpleName}: " +
-                        "currentPriority=$currentPriority; config=$config"
-                )
-            }
-            apply()
-            return
-        }
-
-        if (DEBUG) {
-            Log.i(
-                TAG,
-                "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config"
-            )
-        }
-
-        // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
-        // the running set until the copy is started by the handler.
-        runningTransitions.add(transition)
-        transition.addListener(transitionListener)
-        runningPriority = max(currentPriority, config.type.priority)
-
-        handler.post {
-            if (config.terminatePrevious) {
-                TransitionManager.endTransitions(constraintLayout)
-            }
-
-            TransitionManager.beginDelayedTransition(constraintLayout, transition)
-            runningTransitions.remove(transition)
-            apply()
-        }
-    }
-
     private fun logAlphaVisibilityOfAppliedConstraintSet(
         cs: ConstraintSet,
         viewModel: KeyguardClockViewModel
@@ -237,8 +150,6 @@
         )
     }
 
-    companion object {
-        private const val TAG = "KeyguardBlueprintViewBinder"
-        private const val DEBUG = false
-    }
+    private const val TAG = "KeyguardBlueprintViewBinder"
+    private const val DEBUG = false
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 39db22d..fc92afe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -22,6 +22,7 @@
 import android.annotation.SuppressLint
 import android.graphics.Point
 import android.graphics.Rect
+import android.util.Log
 import android.view.HapticFeedbackConstants
 import android.view.View
 import android.view.View.OnLayoutChangeListener
@@ -56,8 +57,11 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
+import com.android.systemui.keyguard.ui.viewmodel.TransitionData
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
@@ -93,12 +97,14 @@
     fun bind(
         view: ViewGroup,
         viewModel: KeyguardRootViewModel,
+        blueprintViewModel: KeyguardBlueprintViewModel,
         configuration: ConfigurationState,
         occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?,
         chipbarCoordinator: ChipbarCoordinator?,
         screenOffAnimationController: ScreenOffAnimationController,
         shadeInteractor: ShadeInteractor,
         clockInteractor: KeyguardClockInteractor,
+        clockViewModel: KeyguardClockViewModel,
         interactionJankMonitor: InteractionJankMonitor?,
         deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
         vibratorHelper: VibratorHelper?,
@@ -348,7 +354,16 @@
             }
         }
 
-        disposables += view.onLayoutChanged(OnLayoutChange(viewModel, childViews, burnInParams))
+        disposables +=
+            view.onLayoutChanged(
+                OnLayoutChange(
+                    viewModel,
+                    blueprintViewModel,
+                    clockViewModel,
+                    childViews,
+                    burnInParams
+                )
+            )
 
         // Views will be added or removed after the call to bind(). This is needed to avoid many
         // calls to findViewById
@@ -404,9 +419,13 @@
 
     private class OnLayoutChange(
         private val viewModel: KeyguardRootViewModel,
+        private val blueprintViewModel: KeyguardBlueprintViewModel,
+        private val clockViewModel: KeyguardClockViewModel,
         private val childViews: Map<Int, View>,
         private val burnInParams: MutableStateFlow<BurnInParameters>,
     ) : OnLayoutChangeListener {
+        var prevTransition: TransitionData? = null
+
         override fun onLayoutChange(
             view: View,
             left: Int,
@@ -418,11 +437,21 @@
             oldRight: Int,
             oldBottom: Int
         ) {
+            // After layout, ensure the notifications are positioned correctly
             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
-                // After layout, ensure the notifications are positioned correctly
+                // Do not update a second time while a blueprint transition is running
+                val transition = blueprintViewModel.currentTransition.value
+                val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
+                if (prevTransition == transition && shouldAnimate) {
+                    if (DEBUG) Log.w(TAG, "Skipping; layout during transition")
+                    return
+                }
+
+                prevTransition = transition
                 viewModel.onNotificationContainerBoundsChanged(
                     notificationListPlaceholder.top.toFloat(),
                     notificationListPlaceholder.bottom.toFloat(),
+                    animate = shouldAnimate
                 )
             }
 
@@ -585,4 +614,6 @@
 
     private const val ID = "occluding_app_device_entry_unlock_msg"
     private const val AOD_ICONS_APPEAR_DURATION: Long = 200
+    private const val TAG = "KeyguardRootViewBinder"
+    private const val DEBUG = false
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index fb1853f..777c873 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -68,7 +68,9 @@
 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
@@ -134,6 +136,7 @@
     private val vibratorHelper: VibratorHelper,
     private val indicationController: KeyguardIndicationController,
     private val keyguardRootViewModel: KeyguardRootViewModel,
+    private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
     @Assisted bundle: Bundle,
     private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
     private val chipbarCoordinator: ChipbarCoordinator,
@@ -143,6 +146,7 @@
     private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
     private val defaultShortcutsSection: DefaultShortcutsSection,
     private val keyguardClockInteractor: KeyguardClockInteractor,
+    private val keyguardClockViewModel: KeyguardClockViewModel,
 ) {
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
     private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -379,12 +383,14 @@
                 KeyguardRootViewBinder.bind(
                     keyguardRootView,
                     keyguardRootViewModel,
+                    keyguardBlueprintViewModel,
                     configuration,
                     occludingAppDeviceEntryMessageViewModel,
                     chipbarCoordinator,
                     screenOffAnimationController,
                     shadeInteractor,
                     keyguardClockInteractor,
+                    keyguardClockViewModel,
                     null, // jank monitor not required for preview mode
                     null, // device entry haptics not required preview mode
                     null, // device entry haptics not required for preview mode
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index 02e9ca5..39f1ebe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -31,16 +31,16 @@
 
     enum class Type(
         val priority: Int,
+        val animateNotifChanges: Boolean,
     ) {
-        ClockSize(100),
-        ClockCenter(99),
-        DefaultClockStepping(98),
-        AodNotifIconsTransition(97),
-        SmartspaceVisibility(2),
-        DefaultTransition(1),
+        ClockSize(100, true),
+        ClockCenter(99, false),
+        DefaultClockStepping(98, false),
+        SmartspaceVisibility(2, true),
+        DefaultTransition(1, false),
         // When transition between blueprint, we don't need any duration or interpolator but we need
         // all elements go to correct state
-        NoTransition(0),
+        NoTransition(0, false),
     }
 
     data class Config(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 2832e9d..d77b548 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -19,6 +19,8 @@
 
 import android.content.Context
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -29,6 +31,7 @@
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
@@ -38,6 +41,7 @@
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import com.android.systemui.util.ui.value
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 
@@ -51,6 +55,7 @@
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val notificationIconAreaController: NotificationIconAreaController,
     private val systemBarUtilsState: SystemBarUtilsState,
+    private val rootViewModel: KeyguardRootViewModel,
 ) : KeyguardSection() {
 
     private var nicBindingDisposable: DisposableHandle? = null
@@ -101,20 +106,14 @@
         if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
+
         val bottomMargin =
             context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
-
-        val useSplitShade = context.resources.getBoolean(R.bool.config_use_split_notification_shade)
-
-        val topAlignment =
-            if (useSplitShade) {
-                TOP
-            } else {
-                BOTTOM
-            }
+        val isVisible = rootViewModel.isNotifIconContainerVisible.value
         constraintSet.apply {
             connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin)
             setGoneMargin(nicId, BOTTOM, bottomMargin)
+            setVisibility(nicId, if (isVisible.value) VISIBLE else GONE)
 
             connect(
                 nicId,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 7c745bc..f17dbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -369,6 +369,21 @@
             addTarget(R.id.status_view_media_container)
         }
 
+        override fun mutateBounds(
+            view: View,
+            fromIsVis: Boolean,
+            toIsVis: Boolean,
+            fromBounds: Rect,
+            toBounds: Rect,
+            fromSSBounds: Rect?,
+            toSSBounds: Rect?
+        ) {
+            // If view is changing visibility, hold it in place
+            if (fromIsVis == toIsVis) return
+            if (DEBUG) Log.i(TAG, "Holding position of ${view.id}")
+            toBounds.set(fromBounds)
+        }
+
         companion object {
             const val STATUS_AREA_MOVE_UP_MILLIS = 967L
             const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index b1f1898..7ac03bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -17,15 +17,119 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.os.Handler
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.util.Log
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+data class TransitionData(
+    val config: Config,
+    val start: Long = System.currentTimeMillis(),
+)
 
 class KeyguardBlueprintViewModel
 @Inject
 constructor(
+    @Main private val handler: Handler,
     keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
     val refreshTransition = keyguardBlueprintInteractor.refreshTransition
+
+    private val _currentTransition = MutableStateFlow<TransitionData?>(null)
+    val currentTransition = _currentTransition.asStateFlow()
+
+    private val runningTransitions = mutableSetOf<Transition>()
+    private val transitionListener =
+        object : Transition.TransitionListener {
+            override fun onTransitionCancel(transition: Transition) {
+                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
+                updateTransitions(null) { remove(transition) }
+            }
+
+            override fun onTransitionEnd(transition: Transition) {
+                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+                updateTransitions(null) { remove(transition) }
+            }
+
+            override fun onTransitionPause(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}")
+                updateTransitions(null) { remove(transition) }
+            }
+
+            override fun onTransitionResume(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}")
+                updateTransitions(null) { add(transition) }
+            }
+
+            override fun onTransitionStart(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}")
+                updateTransitions(null) { add(transition) }
+            }
+        }
+
+    fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
+        runningTransitions.mutate()
+
+        if (runningTransitions.size <= 0) _currentTransition.value = null
+        else if (data != null) _currentTransition.value = data
+    }
+
+    fun runTransition(
+        constraintLayout: ConstraintLayout,
+        transition: Transition,
+        config: Config,
+        apply: () -> Unit,
+    ) {
+        val currentPriority = currentTransition.value?.let { it.config.type.priority } ?: -1
+        if (config.checkPriority && config.type.priority < currentPriority) {
+            if (DEBUG) {
+                Log.w(
+                    TAG,
+                    "runTransition: skipping ${transition::class.simpleName}: " +
+                        "currentPriority=$currentPriority; config=$config"
+                )
+            }
+            apply()
+            return
+        }
+
+        if (DEBUG) {
+            Log.i(
+                TAG,
+                "runTransition: running ${transition::class.simpleName}: " +
+                    "currentPriority=$currentPriority; config=$config"
+            )
+        }
+
+        // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
+        // the running set until the copy is started by the handler.
+        updateTransitions(TransitionData(config)) { add(transition) }
+        transition.addListener(transitionListener)
+
+        handler.post {
+            if (config.terminatePrevious) {
+                TransitionManager.endTransitions(constraintLayout)
+            }
+
+            TransitionManager.beginDelayedTransition(constraintLayout, transition)
+            apply()
+
+            // Delay removal until after copied transition has started
+            handler.post { updateTransitions(null) { remove(transition) } }
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardBlueprintViewModel"
+        private const val DEBUG = true
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index aaec69f..1ec2a49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -58,6 +58,8 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combineTransform
@@ -67,13 +69,14 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class KeyguardRootViewModel
 @Inject
 constructor(
-    @Application private val scope: CoroutineScope,
+    @Application private val applicationScope: CoroutineScope,
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val dozeParameters: DozeParameters,
     private val keyguardInteractor: KeyguardInteractor,
@@ -280,7 +283,7 @@
         burnInJob?.cancel()
 
         burnInJob =
-            scope.launch("$TAG#aodBurnInViewModel") {
+            applicationScope.launch("$TAG#aodBurnInViewModel") {
                 aodBurnInViewModel.movement(params).collect { _burnInModel.value = it }
             }
     }
@@ -294,7 +297,7 @@
         }
 
     /** Is the notification icon container visible? */
-    val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> =
+    val isNotifIconContainerVisible: StateFlow<AnimatedValue<Boolean>> =
         combine(
                 goneToAodTransitionRunning,
                 keyguardTransitionInteractor.finishedKeyguardState.map {
@@ -336,11 +339,15 @@
                         }
                 }
             }
-            .distinctUntilChanged()
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = AnimatedValue.NotAnimating(false),
+            )
 
-    fun onNotificationContainerBoundsChanged(top: Float, bottom: Float) {
+    fun onNotificationContainerBoundsChanged(top: Float, bottom: Float, animate: Boolean = false) {
         keyguardInteractor.setNotificationContainerBounds(
-            NotificationContainerBounds(top = top, bottom = bottom)
+            NotificationContainerBounds(top = top, bottom = bottom, isAnimated = animate)
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
index a18b033..ec2a1d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
@@ -17,9 +17,11 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.os.fakeExecutorHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.testKosmos
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -33,12 +35,16 @@
 class KeyguardBlueprintViewModelTest : SysuiTestCase() {
     @Mock private lateinit var keyguardBlueprintInteractor: KeyguardBlueprintInteractor
     private lateinit var undertest: KeyguardBlueprintViewModel
+    private val kosmos = testKosmos()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         undertest =
-            KeyguardBlueprintViewModel(keyguardBlueprintInteractor = keyguardBlueprintInteractor)
+            KeyguardBlueprintViewModel(
+                handler = kosmos.fakeExecutorHandler,
+                keyguardBlueprintInteractor = keyguardBlueprintInteractor,
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinderKosmos.kt
deleted file mode 100644
index 24d2c2f..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinderKosmos.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.keyguard.ui.binder
-
-import android.os.fakeExecutorHandler
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.keyguardBlueprintViewBinder by
-    Kosmos.Fixture { KeyguardBlueprintViewBinder(fakeExecutorHandler) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
index 63b87c0..0c538ff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
@@ -16,8 +16,14 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.os.fakeExecutorHandler
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.keyguardBlueprintViewModel by
-    Kosmos.Fixture { KeyguardBlueprintViewModel(keyguardBlueprintInteractor) }
+    Kosmos.Fixture {
+        KeyguardBlueprintViewModel(
+            fakeExecutorHandler,
+            keyguardBlueprintInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index f856d27..2567ffe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -32,7 +32,7 @@
 
 val Kosmos.keyguardRootViewModel by Fixture {
     KeyguardRootViewModel(
-        scope = applicationCoroutineScope,
+        applicationScope = applicationCoroutineScope,
         deviceEntryInteractor = deviceEntryInteractor,
         dozeParameters = dozeParameters,
         keyguardInteractor = keyguardInteractor,