Merge "Fix flicker when starting folding" into tm-qpr-dev
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 3f6046f..53a84bd 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -583,6 +583,15 @@
     mFlingerSurface = s;
     mTargetInset = -1;
 
+    // Rotate the boot animation according to the value specified in the sysprop
+    // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0,
+    // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270.
+    // If the value isn't specified or is ORIENTATION_0, nothing will be changed.
+    // This is needed to support having boot animation in orientations different from the natural
+    // device orientation. For example, on tablets that may want to keep natural orientation
+    // portrait for applications compatibility and to have the boot animation in landscape.
+    rotateAwayFromNaturalOrientationIfNeeded();
+
     projectSceneToWindow();
 
     // Register a display event receiver
@@ -596,6 +605,50 @@
     return NO_ERROR;
 }
 
+void BootAnimation::rotateAwayFromNaturalOrientationIfNeeded() {
+    const auto orientation = parseOrientationProperty();
+
+    if (orientation == ui::ROTATION_0) {
+        // Do nothing if the sysprop isn't set or is set to ROTATION_0.
+        return;
+    }
+
+    if (orientation == ui::ROTATION_90 || orientation == ui::ROTATION_270) {
+        std::swap(mWidth, mHeight);
+        std::swap(mInitWidth, mInitHeight);
+        mFlingerSurfaceControl->updateDefaultBufferSize(mWidth, mHeight);
+    }
+
+    Rect displayRect(0, 0, mWidth, mHeight);
+    Rect layerStackRect(0, 0, mWidth, mHeight);
+
+    SurfaceComposerClient::Transaction t;
+    t.setDisplayProjection(mDisplayToken, orientation, layerStackRect, displayRect);
+    t.apply();
+}
+
+ui::Rotation BootAnimation::parseOrientationProperty() {
+    const auto displayIds = SurfaceComposerClient::getPhysicalDisplayIds();
+    if (displayIds.size() == 0) {
+        return ui::ROTATION_0;
+    }
+    const auto displayId = displayIds[0];
+    const auto syspropName = [displayId] {
+        std::stringstream ss;
+        ss << "ro.bootanim.set_orientation_" << displayId.value;
+        return ss.str();
+    }();
+    const auto syspropValue = android::base::GetProperty(syspropName, "ORIENTATION_0");
+    if (syspropValue == "ORIENTATION_90") {
+        return ui::ROTATION_90;
+    } else if (syspropValue == "ORIENTATION_180") {
+        return ui::ROTATION_180;
+    } else if (syspropValue == "ORIENTATION_270") {
+        return ui::ROTATION_270;
+    }
+    return ui::ROTATION_0;
+}
+
 void BootAnimation::projectSceneToWindow() {
     glViewport(0, 0, mWidth, mHeight);
     glScissor(0, 0, mWidth, mHeight);
diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h
index 8658205..8683b71 100644
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -30,6 +30,8 @@
 #include <utils/Thread.h>
 #include <binder/IBinder.h>
 
+#include <ui/Rotation.h>
+
 #include <EGL/egl.h>
 #include <GLES2/gl2.h>
 
@@ -200,6 +202,8 @@
     ui::Size limitSurfaceSize(int width, int height) const;
     void resizeSurface(int newWidth, int newHeight);
     void projectSceneToWindow();
+    void rotateAwayFromNaturalOrientationIfNeeded();
+    ui::Rotation parseOrientationProperty();
 
     bool shouldStopPlayingPart(const Animation::Part& part, int fadedFramesCount,
                                int lastDisplayedProgress);
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 162a997..7eacc3c 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1096,7 +1096,7 @@
     }
 
     /**
-     * Like {@link #getFastDrawable(int)}, but the returned Drawable has a number
+     * Like {@link #getDrawable(int)}, but the returned Drawable has a number
      * of limitations to reduce its overhead as much as possible. It will
      * never scale the wallpaper (only centering it if the requested bounds
      * do match the bitmap bounds, which should not be typical), doesn't
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 30b7d25..be99f0f 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -114,7 +114,7 @@
      */
     @ChangeId
     @Overridable
-    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
     @TestApi
     public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L;
 
diff --git a/core/java/android/service/wallpaper/OWNERS b/core/java/android/service/wallpaper/OWNERS
index 756eef8..71bd190 100644
--- a/core/java/android/service/wallpaper/OWNERS
+++ b/core/java/android/service/wallpaper/OWNERS
@@ -3,3 +3,6 @@
 dupin@google.com
 dsandler@android.com
 dsandler@google.com
+pomini@google.com
+poultney@google.com
+santie@google.com
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b0f6ae6..ba274a7 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2331,6 +2331,7 @@
   <java-symbol type="drawable" name="scrubber_control_selector_holo" />
   <java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
   <java-symbol type="drawable" name="progress_small_material" />
+  <java-symbol type="drawable" name="ic_chevron_end" />
   <java-symbol type="string" name="chooseUsbActivity" />
   <java-symbol type="string" name="ext_media_badremoval_notification_message" />
   <java-symbol type="string" name="ext_media_badremoval_notification_title" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b075b14..3341470 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -17,12 +17,20 @@
 package com.android.wm.shell.desktopmode
 
 import android.app.ActivityManager
-import android.app.WindowConfiguration
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.app.WindowConfiguration.WindowingMode
 import android.content.Context
-import android.view.WindowManager
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
 import com.android.internal.protolog.common.ProtoLog
@@ -51,7 +59,7 @@
     private val transitions: Transitions,
     private val desktopModeTaskRepository: DesktopModeTaskRepository,
     @ShellMainThread private val mainExecutor: ShellExecutor
-) : RemoteCallable<DesktopTasksController> {
+) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
 
     private val desktopMode: DesktopModeImpl
 
@@ -69,6 +77,7 @@
             { createExternalInterface() },
             this
         )
+        transitions.addHandler(this)
     }
 
     /** Show all tasks, that are part of the desktop, on top of launcher */
@@ -81,7 +90,7 @@
         // Execute transaction if there are pending operations
         if (!wct.isEmpty) {
             if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-                transitions.startTransition(WindowManager.TRANSIT_TO_FRONT, wct, null /* handler */)
+                transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
             } else {
                 shellTaskOrganizer.applyTransaction(wct)
             }
@@ -101,11 +110,11 @@
         // Bring other apps to front first
         bringDesktopAppsToFront(wct)
 
-        wct.setWindowingMode(task.getToken(), WindowConfiguration.WINDOWING_MODE_FREEFORM)
+        wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FREEFORM)
         wct.reorder(task.getToken(), true /* onTop */)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, null /* handler */)
+            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
         }
@@ -121,10 +130,10 @@
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId)
 
         val wct = WindowContainerTransaction()
-        wct.setWindowingMode(task.getToken(), WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+        wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FULLSCREEN)
         wct.setBounds(task.getToken(), null)
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, null /* handler */)
+            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
         }
@@ -181,6 +190,80 @@
         return mainExecutor
     }
 
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback
+    ): Boolean {
+        // This handler should never be the sole handler, so should not animate anything.
+        return false
+    }
+
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo
+    ): WindowContainerTransaction? {
+        // Check if we should skip handling this transition
+        val task: ActivityManager.RunningTaskInfo? = request.triggerTask
+        val shouldHandleRequest =
+            when {
+                // Only handle open or to front transitions
+                request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false
+                // Only handle when it is a task transition
+                task == null -> false
+                // Only handle standard type tasks
+                task.activityType != ACTIVITY_TYPE_STANDARD -> false
+                // Only handle fullscreen or freeform tasks
+                task.windowingMode != WINDOWING_MODE_FULLSCREEN &&
+                    task.windowingMode != WINDOWING_MODE_FREEFORM -> false
+                // Otherwise process it
+                else -> true
+            }
+
+        if (!shouldHandleRequest) {
+            return null
+        }
+
+        val activeTasks = desktopModeTaskRepository.getActiveTasks()
+
+        // Check if we should switch a fullscreen task to freeform
+        if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // If there are any visible desktop tasks, switch the task to freeform
+            if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+                ProtoLog.d(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTasksController#handleRequest: switch fullscreen task to freeform," +
+                        " taskId=%d",
+                    task.taskId
+                )
+                return WindowContainerTransaction().apply {
+                    setWindowingMode(task.token, WINDOWING_MODE_FREEFORM)
+                }
+            }
+        }
+
+        // CHeck if we should switch a freeform task to fullscreen
+        if (task?.windowingMode == WINDOWING_MODE_FREEFORM) {
+            // If no visible desktop tasks, switch this task to freeform as the transition came
+            // outside of this controller
+            if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+                ProtoLog.d(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTasksController#handleRequest: switch freeform task to fullscreen," +
+                        " taskId=%d",
+                    task.taskId
+                )
+                return WindowContainerTransaction().apply {
+                    setWindowingMode(task.token, WINDOWING_MODE_FULLSCREEN)
+                    setBounds(task.token, null)
+                }
+            }
+        }
+        return null
+    }
+
     /** Creates a new instance of the external interface to pass to another process. */
     private fun createExternalInterface(): ExternalInterfaceBinder {
         return IDesktopModeImpl(this)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index de2473b..9a92879 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -17,10 +17,18 @@
 package com.android.wm.shell.desktopmode
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.os.Binder
 import android.testing.AndroidTestingRunner
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
 import androidx.test.filters.SmallTest
@@ -29,6 +37,7 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
@@ -37,9 +46,11 @@
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.After
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -79,6 +90,7 @@
         desktopModeTaskRepository = DesktopModeTaskRepository()
 
         whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+        whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
 
         controller = createController()
 
@@ -221,6 +233,114 @@
         assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
     }
 
+    @Test
+    fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+
+        val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+        assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val freeformTask = setUpFreeformTask()
+        markTaskHidden(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val fullscreenTask = createFullscreenTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+    }
+
+    @Test
+    fun handleRequest_freeformTask_freeformVisible_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val freeformTask1 = setUpFreeformTask()
+        markTaskVisible(freeformTask1)
+
+        val freeformTask2 = createFreeformTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull()
+    }
+
+    @Test
+    fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val freeformTask1 = setUpFreeformTask()
+        markTaskHidden(freeformTask1)
+
+        val freeformTask2 = createFreeformTask()
+        val result =
+            controller.handleRequest(
+                Binder(),
+                createTransition(freeformTask2, type = TRANSIT_TO_FRONT)
+            )
+        assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+    }
+
+    @Test
+    fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val task = createFreeformTask()
+        val result = controller.handleRequest(Binder(), createTransition(task))
+        assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+    }
+
+    @Test
+    fun handleRequest_notOpenOrToFrontTransition_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val task =
+            TestRunningTaskInfoBuilder()
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .build()
+        val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE)
+        val result = controller.handleRequest(Binder(), transition)
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun handleRequest_noTriggerTask_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+        assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
+    }
+
+    @Test
+    fun handleRequest_triggerTaskNotStandard_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+        val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+        assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+    }
+
+    @Test
+    fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val task =
+            TestRunningTaskInfoBuilder()
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+                .build()
+        assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+    }
+
     private fun setUpFreeformTask(): RunningTaskInfo {
         val task = createFreeformTask()
         whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -254,7 +374,7 @@
 
     private fun getLatestWct(): WindowContainerTransaction {
         val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+        if (ENABLE_SHELL_TRANSITIONS) {
             verify(transitions).startTransition(anyInt(), arg.capture(), isNull())
         } else {
             verify(shellTaskOrganizer).applyTransaction(arg.capture())
@@ -263,12 +383,19 @@
     }
 
     private fun verifyWCTNotExecuted() {
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+        if (ENABLE_SHELL_TRANSITIONS) {
             verify(transitions, never()).startTransition(anyInt(), any(), isNull())
         } else {
             verify(shellTaskOrganizer, never()).applyTransaction(any())
         }
     }
+
+    private fun createTransition(
+        task: RunningTaskInfo?,
+        @WindowManager.TransitionType type: Int = TRANSIT_OPEN
+    ): TransitionRequestInfo {
+        return TransitionRequestInfo(type, task, null /* remoteTransition */)
+    }
 }
 
 private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
index 4da47fd..db224be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
@@ -66,6 +66,8 @@
             "com.android.settings.category.ia.battery_saver_settings";
     public static final String CATEGORY_SMART_BATTERY_SETTINGS =
             "com.android.settings.category.ia.smart_battery_settings";
+    public static final String CATEGORY_COMMUNAL_SETTINGS =
+            "com.android.settings.category.ia.communal";
 
     public static final Map<String, String> KEY_COMPAT_MAP;
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
index 340a6c7..c9dc1ba 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
@@ -60,8 +60,9 @@
         allKeys.add(CategoryKey.CATEGORY_GESTURES);
         allKeys.add(CategoryKey.CATEGORY_NIGHT_DISPLAY);
         allKeys.add(CategoryKey.CATEGORY_SMART_BATTERY_SETTINGS);
+        allKeys.add(CategoryKey.CATEGORY_COMMUNAL_SETTINGS);
         // DO NOT REMOVE ANYTHING ABOVE
 
-        assertThat(allKeys.size()).isEqualTo(19);
+        assertThat(allKeys.size()).isEqualTo(20);
     }
 }
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
index 18534f4..d31ca51 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
@@ -24,6 +24,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.defaultMinSize
@@ -62,6 +63,7 @@
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.scale
 import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.findRootCoordinates
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.ComposeView
@@ -118,12 +120,14 @@
     contentColor: Color = contentColorFor(color),
     borderStroke: BorderStroke? = null,
     onClick: ((Expandable) -> Unit)? = null,
+    interactionSource: MutableInteractionSource? = null,
     content: @Composable (Expandable) -> Unit,
 ) {
     Expandable(
         rememberExpandableController(color, shape, contentColor, borderStroke),
         modifier,
         onClick,
+        interactionSource,
         content,
     )
 }
@@ -158,6 +162,7 @@
     controller: ExpandableController,
     modifier: Modifier = Modifier,
     onClick: ((Expandable) -> Unit)? = null,
+    interactionSource: MutableInteractionSource? = null,
     content: @Composable (Expandable) -> Unit,
 ) {
     val controller = controller as ExpandableControllerImpl
@@ -190,6 +195,18 @@
 
     var thisExpandableSize by remember { mutableStateOf(Size.Zero) }
 
+    /** Set the current element size as this Expandable size. */
+    fun Modifier.updateExpandableSize(): Modifier {
+        return this.onGloballyPositioned { coords ->
+            thisExpandableSize =
+                coords
+                    .findRootCoordinates()
+                    // Make sure that we report the actual size, and not the visual/clipped one.
+                    .localBoundingBoxOf(coords, clipBounds = false)
+                    .size
+        }
+    }
+
     // Make sure we don't read animatorState directly here to avoid recomposition every time the
     // state changes (i.e. every frame of the animation).
     val isAnimating by remember {
@@ -247,7 +264,7 @@
         controller.isDialogShowing.value -> {
             Box(
                 modifier
-                    .onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
+                    .updateExpandableSize()
                     .then(minInteractiveSizeModifier)
                     .drawWithContent { /* Don't draw anything when the dialog is shown. */}
                     .onGloballyPositioned {
@@ -258,18 +275,25 @@
         else -> {
             val clickModifier =
                 if (onClick != null) {
-                    Modifier.clickable { onClick(controller.expandable) }
+                    if (interactionSource != null) {
+                        // If the caller provided an interaction source, then that means that they
+                        // will draw the click indication themselves.
+                        Modifier.clickable(interactionSource, indication = null) {
+                            onClick(controller.expandable)
+                        }
+                    } else {
+                        // If no interaction source is provided, we draw the default indication (a
+                        // ripple) and make sure it's clipped by the expandable shape.
+                        Modifier.clip(shape).clickable { onClick(controller.expandable) }
+                    }
                 } else {
                     Modifier
                 }
 
             Box(
                 modifier
-                    .onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
+                    .updateExpandableSize()
                     .then(minInteractiveSizeModifier)
-                    // Note that clip() *must* be above the clickModifier to properly clip the
-                    // ripple.
-                    .clip(shape)
                     .then(clickModifier)
                     .background(color, shape)
                     .border(controller)
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
index b8639e6..caa7e5f 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
@@ -65,10 +65,12 @@
     val colorForeground = getColor(context, R.attr.colorForeground)
     val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
 
-    private fun getColor(context: Context, attr: Int): Color {
-        val ta = context.obtainStyledAttributes(intArrayOf(attr))
-        @ColorInt val color = ta.getColor(0, 0)
-        ta.recycle()
-        return Color(color)
+    companion object {
+        fun getColor(context: Context, attr: Int): Color {
+            val ta = context.obtainStyledAttributes(intArrayOf(attr))
+            @ColorInt val color = ta.getColor(0, 0)
+            ta.recycle()
+            return Color(color)
+        }
     }
 }
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt
new file mode 100644
index 0000000..de47cce
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 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.compose.theme
+
+import android.annotation.AttrRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+/** Read the [Color] from the given [attribute]. */
+@Composable
+@ReadOnlyComposable
+fun colorAttr(@AttrRes attribute: Int): Color {
+    return AndroidColorScheme.getColor(LocalContext.current, attribute)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt
new file mode 100644
index 0000000..4a5ad65
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 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.common.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.systemui.common.shared.model.ContentDescription
+
+/** Returns the loaded [String] or `null` if there isn't one. */
+@Composable
+fun ContentDescription.load(): String? {
+    return when (this) {
+        is ContentDescription.Loaded -> description
+        is ContentDescription.Resource -> stringResource(res)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
new file mode 100644
index 0000000..6e83124
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 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.common.ui.compose
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
+import androidx.core.graphics.drawable.toBitmap
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Icon composable that draws [icon] using [tint].
+ *
+ * Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors.
+ */
+@Composable
+fun Icon(
+    icon: Icon,
+    modifier: Modifier = Modifier,
+    tint: Color = LocalContentColor.current,
+) {
+    val contentDescription = icon.contentDescription?.load()
+    when (icon) {
+        is Icon.Loaded -> {
+            Icon(icon.drawable.toBitmap().asImageBitmap(), contentDescription, modifier, tint)
+        }
+        is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 2bf1937..2aac46e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -53,7 +53,6 @@
 import com.android.systemui.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
-import kotlinx.coroutines.flow.collect
 
 /**
  * Compose the screen associated to a [PeopleViewModel].
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
new file mode 100644
index 0000000..654b723
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2022 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.qs.footer.ui.compose
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.compose.animation.Expandable
+import com.android.systemui.compose.modifiers.background
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.compose.theme.colorAttr
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import kotlinx.coroutines.launch
+
+/** The Quick Settings footer actions row. */
+@Composable
+fun FooterActions(
+    viewModel: FooterActionsViewModel,
+    qsVisibilityLifecycleOwner: LifecycleOwner,
+    modifier: Modifier = Modifier,
+) {
+    val context = LocalContext.current
+
+    // Collect visibility and alphas as soon as we are composed, even when not visible.
+    val isVisible by viewModel.isVisible.collectAsState()
+    val alpha by viewModel.alpha.collectAsState()
+    val backgroundAlpha = viewModel.backgroundAlpha.collectAsState()
+
+    var security by remember { mutableStateOf<FooterActionsSecurityButtonViewModel?>(null) }
+    var foregroundServices by remember {
+        mutableStateOf<FooterActionsForegroundServicesButtonViewModel?>(null)
+    }
+    var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) }
+
+    LaunchedEffect(
+        context,
+        qsVisibilityLifecycleOwner,
+        viewModel,
+        viewModel.security,
+        viewModel.foregroundServices,
+        viewModel.userSwitcher,
+    ) {
+        launch {
+            // Listen for dialog requests as soon as we are composed, even when not visible.
+            viewModel.observeDeviceMonitoringDialogRequests(context)
+        }
+
+        // Listen for model changes only when QS are visible.
+        qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+            launch { viewModel.security.collect { security = it } }
+            launch { viewModel.foregroundServices.collect { foregroundServices = it } }
+            launch { viewModel.userSwitcher.collect { userSwitcher = it } }
+        }
+    }
+
+    val backgroundColor = colorAttr(R.attr.underSurfaceColor)
+    val contentColor = LocalAndroidColorScheme.current.textColorPrimary
+    val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
+    val backgroundModifier =
+        remember(
+            backgroundColor,
+            backgroundAlpha,
+            backgroundTopRadius,
+        ) {
+            Modifier.background(
+                backgroundColor,
+                backgroundAlpha::value,
+                RoundedCornerShape(topStart = backgroundTopRadius, topEnd = backgroundTopRadius),
+            )
+        }
+
+    Row(
+        modifier
+            .fillMaxWidth()
+            .graphicsLayer { this.alpha = alpha }
+            .drawWithContent {
+                if (isVisible) {
+                    drawContent()
+                }
+            }
+            .then(backgroundModifier)
+            .padding(
+                top = dimensionResource(R.dimen.qs_footer_actions_top_padding),
+                bottom = dimensionResource(R.dimen.qs_footer_actions_bottom_padding),
+            )
+            .layout { measurable, constraints ->
+                // All buttons have a 4dp padding to increase their touch size. To be consistent
+                // with the View implementation, we want to left-most and right-most buttons to be
+                // visually aligned with the left and right sides of this row. So we let this
+                // component be 2*4dp wider and then offset it by -4dp to the start.
+                val inset = 4.dp.roundToPx()
+                val additionalWidth = inset * 2
+                val newConstraints =
+                    if (constraints.hasBoundedWidth) {
+                        constraints.copy(maxWidth = constraints.maxWidth + additionalWidth)
+                    } else {
+                        constraints
+                    }
+                val placeable = measurable.measure(newConstraints)
+
+                val width = constraints.constrainWidth(placeable.width - additionalWidth)
+                val height = constraints.constrainHeight(placeable.height)
+                layout(width, height) { placeable.place(-inset, 0) }
+            },
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        CompositionLocalProvider(
+            LocalContentColor provides contentColor,
+        ) {
+            if (security == null && foregroundServices == null) {
+                Spacer(Modifier.weight(1f))
+            }
+
+            security?.let { SecurityButton(it, Modifier.weight(1f)) }
+            foregroundServices?.let { ForegroundServicesButton(it) }
+            userSwitcher?.let { IconButton(it) }
+            IconButton(viewModel.settings)
+            viewModel.power?.let { IconButton(it) }
+        }
+    }
+}
+
+/** The security button. */
+@Composable
+private fun SecurityButton(
+    model: FooterActionsSecurityButtonViewModel,
+    modifier: Modifier = Modifier,
+) {
+    val onClick: ((Expandable) -> Unit)? =
+        model.onClick?.let { onClick ->
+            val context = LocalContext.current
+            { expandable -> onClick(context, expandable) }
+        }
+
+    TextButton(
+        model.icon,
+        model.text,
+        showNewDot = false,
+        onClick = onClick,
+        modifier,
+    )
+}
+
+/** The foreground services button. */
+@Composable
+private fun RowScope.ForegroundServicesButton(
+    model: FooterActionsForegroundServicesButtonViewModel,
+) {
+    if (model.displayText) {
+        TextButton(
+            Icon.Resource(R.drawable.ic_info_outline, contentDescription = null),
+            model.text,
+            showNewDot = model.hasNewChanges,
+            onClick = model.onClick,
+            Modifier.weight(1f),
+        )
+    } else {
+        NumberButton(
+            model.foregroundServicesCount,
+            showNewDot = model.hasNewChanges,
+            onClick = model.onClick,
+        )
+    }
+}
+
+/** A button with an icon. */
+@Composable
+private fun IconButton(
+    model: FooterActionsButtonViewModel,
+    modifier: Modifier = Modifier,
+) {
+    Expandable(
+        color = colorAttr(model.backgroundColor),
+        shape = CircleShape,
+        onClick = model.onClick,
+        modifier = modifier,
+    ) {
+        val tint = model.iconTint?.let { Color(it) } ?: Color.Unspecified
+        Icon(
+            model.icon,
+            tint = tint,
+            modifier = Modifier.size(20.dp),
+        )
+    }
+}
+
+/** A button with a number an an optional dot (to indicate new changes). */
+@Composable
+private fun NumberButton(
+    number: Int,
+    showNewDot: Boolean,
+    onClick: (Expandable) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    // By default Expandable will show a ripple above its content when clicked, and clip the content
+    // with the shape of the expandable. In this case we also want to show a "new changes dot"
+    // outside of the shape, so we can't clip. To work around that we can pass our own interaction
+    // source and draw the ripple indication ourselves above the text but below the "new changes
+    // dot".
+    val interactionSource = remember { MutableInteractionSource() }
+
+    Expandable(
+        color = colorAttr(R.attr.offStateColor),
+        shape = CircleShape,
+        onClick = onClick,
+        interactionSource = interactionSource,
+        modifier = modifier,
+    ) {
+        Box(Modifier.size(40.dp)) {
+            Box(
+                Modifier.fillMaxSize()
+                    .clip(CircleShape)
+                    .indication(
+                        interactionSource,
+                        LocalIndication.current,
+                    )
+            ) {
+                Text(
+                    number.toString(),
+                    modifier = Modifier.align(Alignment.Center),
+                    style = MaterialTheme.typography.bodyLarge,
+                    color = LocalAndroidColorScheme.current.textColorPrimary,
+                    // TODO(b/242040009): This should only use a standard text style instead and
+                    // should not override the text size.
+                    fontSize = 18.sp,
+                )
+            }
+
+            if (showNewDot) {
+                NewChangesDot(Modifier.align(Alignment.BottomEnd))
+            }
+        }
+    }
+}
+
+/** A dot that indicates new changes. */
+@Composable
+private fun NewChangesDot(modifier: Modifier = Modifier) {
+    val contentDescription = stringResource(R.string.fgs_dot_content_description)
+    val color = LocalAndroidColorScheme.current.colorAccentTertiary
+
+    Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
+        drawCircle(color)
+    }
+}
+
+/** A larger button with an icon, some text and an optional dot (to indicate new changes). */
+@Composable
+private fun TextButton(
+    icon: Icon,
+    text: String,
+    showNewDot: Boolean,
+    onClick: ((Expandable) -> Unit)?,
+    modifier: Modifier = Modifier,
+) {
+    Expandable(
+        shape = CircleShape,
+        color = colorAttr(R.attr.underSurfaceColor),
+        contentColor = LocalAndroidColorScheme.current.textColorSecondary,
+        borderStroke = BorderStroke(1.dp, LocalAndroidColorScheme.current.colorBackground),
+        modifier = modifier.padding(horizontal = 4.dp),
+        onClick = onClick,
+    ) {
+        Row(
+            Modifier.padding(horizontal = dimensionResource(R.dimen.qs_footer_padding)),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Icon(icon, Modifier.padding(end = 12.dp).size(20.dp))
+
+            Text(
+                text,
+                Modifier.weight(1f),
+                style = MaterialTheme.typography.bodyMedium,
+                // TODO(b/242040009): Remove this letter spacing. We should only use the M3 text
+                // styles without modifying them.
+                letterSpacing = 0.01.em,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+
+            if (showNewDot) {
+                NewChangesDot(Modifier.padding(start = 8.dp))
+            }
+
+            if (onClick != null) {
+                Icon(
+                    painterResource(com.android.internal.R.drawable.ic_chevron_end),
+                    contentDescription = null,
+                    Modifier.padding(start = 8.dp).size(20.dp),
+                )
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 2d756ae..f0cc42e 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -743,27 +743,6 @@
     <!-- How long in milliseconds before full burn-in protection is achieved. -->
     <integer name="config_dreamOverlayMillisUntilFullJitter">240000</integer>
 
-    <!-- The duration in milliseconds of the y-translation animation when waking up from
-         the dream -->
-    <integer name="config_dreamOverlayOutTranslationYDurationMs">333</integer>
-    <!-- The delay in milliseconds of the y-translation animation when waking up from
-         the dream for the complications at the bottom of the screen -->
-    <integer name="config_dreamOverlayOutTranslationYDelayBottomMs">33</integer>
-    <!-- The delay in milliseconds of the y-translation animation when waking up from
-         the dream for the complications at the top of the screen -->
-    <integer name="config_dreamOverlayOutTranslationYDelayTopMs">117</integer>
-    <!-- The duration in milliseconds of the alpha animation when waking up from the dream -->
-    <integer name="config_dreamOverlayOutAlphaDurationMs">200</integer>
-    <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
-         complications at the top of the screen -->
-    <integer name="config_dreamOverlayOutAlphaDelayTopMs">217</integer>
-    <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
-         complications at the bottom of the screen -->
-    <integer name="config_dreamOverlayOutAlphaDelayBottomMs">133</integer>
-    <!-- The duration in milliseconds of the blur animation when waking up from
-         the dream -->
-    <integer name="config_dreamOverlayOutBlurDurationMs">250</integer>
-
     <integer name="complicationFadeOutMs">500</integer>
 
     <integer name="complicationFadeInMs">500</integer>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index baaef19..7da27b1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -103,7 +103,6 @@
 
     @Override
     public void reset() {
-        super.reset();
         // start fresh
         mDismissing = false;
         mView.resetPasswordText(false /* animate */, false /* announce */);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index b143c5b..d1c9a30 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -121,7 +121,6 @@
 
     @Override
     public void reset() {
-        mMessageAreaController.setMessage("", false);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index f902252..2260e35 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -71,6 +71,7 @@
 import android.os.PowerExemptionManager;
 import android.os.PowerManager;
 import android.os.ServiceManager;
+import android.os.SystemUpdateManager;
 import android.os.UserManager;
 import android.os.Vibrator;
 import android.os.storage.StorageManager;
@@ -139,6 +140,12 @@
         return context.getSystemService(AlarmManager.class);
     }
 
+    @Provides
+    @Singleton
+    static Optional<SystemUpdateManager> provideSystemUpdateManager(Context context) {
+        return Optional.ofNullable(context.getSystemService(SystemUpdateManager.class));
+    }
+
     /** */
     @Provides
     public AmbientDisplayConfiguration provideAmbientDisplayConfiguration(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
new file mode 100644
index 0000000..ab4632b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 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.dreams
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.CallbackController
+import javax.inject.Inject
+
+/** Dream-related callback information */
+@SysUISingleton
+class DreamCallbackController @Inject constructor() :
+    CallbackController<DreamCallbackController.DreamCallback> {
+
+    private val callbacks = mutableSetOf<DreamCallbackController.DreamCallback>()
+
+    override fun addCallback(callback: DreamCallbackController.DreamCallback) {
+        callbacks.add(callback)
+    }
+
+    override fun removeCallback(callback: DreamCallbackController.DreamCallback) {
+        callbacks.remove(callback)
+    }
+
+    fun onWakeUp() {
+        callbacks.forEach { it.onWakeUp() }
+    }
+
+    interface DreamCallback {
+        fun onWakeUp()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 9b8ef71..abe9355 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -22,6 +22,9 @@
 import android.view.View
 import android.view.animation.Interpolator
 import androidx.core.animation.doOnEnd
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dreams.complication.ComplicationHostViewController
 import com.android.systemui.dreams.complication.ComplicationLayoutParams
@@ -29,10 +32,20 @@
 import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP
 import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position
 import com.android.systemui.dreams.dagger.DreamOverlayModule
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_ANIMATION_DURATION
+import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.BlurUtils
 import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import com.android.systemui.util.concurrency.DelayableExecutor
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
 
 /** Controller for dream overlay animations. */
 class DreamOverlayAnimationsController
@@ -43,6 +56,8 @@
     private val mStatusBarViewController: DreamOverlayStatusBarViewController,
     private val mOverlayStateController: DreamOverlayStateController,
     @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
+    private val transitionViewModel: DreamingToLockscreenTransitionViewModel,
+    private val configController: ConfigurationController,
     @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
     private val mDreamInBlurAnimDurationMs: Long,
     @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
@@ -51,22 +66,10 @@
     private val mDreamInTranslationYDistance: Int,
     @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
     private val mDreamInTranslationYDurationMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE)
-    private val mDreamOutTranslationYDistance: Int,
-    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION)
-    private val mDreamOutTranslationYDurationMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
-    private val mDreamOutTranslationYDelayBottomMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
-    private val mDreamOutTranslationYDelayTopMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DURATION) private val mDreamOutAlphaDurationMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_BOTTOM)
-    private val mDreamOutAlphaDelayBottomMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_TOP) private val mDreamOutAlphaDelayTopMs: Long,
-    @Named(DreamOverlayModule.DREAM_OUT_BLUR_DURATION) private val mDreamOutBlurDurationMs: Long
 ) {
 
     private var mAnimator: Animator? = null
+    private lateinit var view: View
 
     /**
      * Store the current alphas at the various positions. This is so that we may resume an animation
@@ -76,9 +79,63 @@
 
     private var mCurrentBlurRadius: Float = 0f
 
+    fun init(view: View) {
+        this.view = view
+
+        view.repeatWhenAttached {
+            val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
+            val configCallback =
+                object : ConfigurationListener {
+                    override fun onDensityOrFontScaleChanged() {
+                        configurationBasedDimensions.value = loadFromResources(view)
+                    }
+                }
+
+            configController.addCallback(configCallback)
+
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                /* Translation animations, when moving from DREAMING->LOCKSCREEN state */
+                launch {
+                    configurationBasedDimensions
+                        .flatMapLatest {
+                            transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
+                        }
+                        .collect { px ->
+                            setElementsTranslationYAtPosition(
+                                px,
+                                ComplicationLayoutParams.POSITION_TOP
+                            )
+                            setElementsTranslationYAtPosition(
+                                px,
+                                ComplicationLayoutParams.POSITION_BOTTOM
+                            )
+                        }
+                }
+
+                /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
+                launch {
+                    transitionViewModel.dreamOverlayAlpha.collect { alpha ->
+                        setElementsAlphaAtPosition(
+                            alpha = alpha,
+                            position = ComplicationLayoutParams.POSITION_TOP,
+                            fadingOut = true,
+                        )
+                        setElementsAlphaAtPosition(
+                            alpha = alpha,
+                            position = ComplicationLayoutParams.POSITION_BOTTOM,
+                            fadingOut = true,
+                        )
+                    }
+                }
+            }
+
+            configController.removeCallback(configCallback)
+        }
+    }
+
     /** Starts the dream content and dream overlay entry animations. */
     @JvmOverloads
-    fun startEntryAnimations(view: View, animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) {
+    fun startEntryAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) {
         cancelAnimations()
 
         mAnimator =
@@ -113,73 +170,9 @@
     }
 
     /** Starts the dream content and dream overlay exit animations. */
-    @JvmOverloads
-    fun startExitAnimations(
-        view: View,
-        doneCallback: () -> Unit,
-        animatorBuilder: () -> AnimatorSet = { AnimatorSet() }
-    ) {
+    fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) {
         cancelAnimations()
-
-        mAnimator =
-            animatorBuilder().apply {
-                playTogether(
-                    blurAnimator(
-                        view = view,
-                        // Start the blurring wherever the entry animation ended, in
-                        // case it was cancelled early.
-                        fromBlurRadius = mCurrentBlurRadius,
-                        toBlurRadius = mDreamBlurRadius.toFloat(),
-                        durationMs = mDreamOutBlurDurationMs,
-                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
-                    ),
-                    translationYAnimator(
-                        from = 0f,
-                        to = mDreamOutTranslationYDistance.toFloat(),
-                        durationMs = mDreamOutTranslationYDurationMs,
-                        delayMs = mDreamOutTranslationYDelayBottomMs,
-                        positions = POSITION_BOTTOM,
-                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
-                    ),
-                    translationYAnimator(
-                        from = 0f,
-                        to = mDreamOutTranslationYDistance.toFloat(),
-                        durationMs = mDreamOutTranslationYDurationMs,
-                        delayMs = mDreamOutTranslationYDelayTopMs,
-                        positions = POSITION_TOP,
-                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
-                    ),
-                    alphaAnimator(
-                        from =
-                            mCurrentAlphaAtPosition.getOrDefault(
-                                key = POSITION_BOTTOM,
-                                defaultValue = 1f
-                            ),
-                        to = 0f,
-                        durationMs = mDreamOutAlphaDurationMs,
-                        delayMs = mDreamOutAlphaDelayBottomMs,
-                        positions = POSITION_BOTTOM
-                    ),
-                    alphaAnimator(
-                        from =
-                            mCurrentAlphaAtPosition.getOrDefault(
-                                key = POSITION_TOP,
-                                defaultValue = 1f
-                            ),
-                        to = 0f,
-                        durationMs = mDreamOutAlphaDurationMs,
-                        delayMs = mDreamOutAlphaDelayTopMs,
-                        positions = POSITION_TOP
-                    )
-                )
-                doOnEnd {
-                    mAnimator = null
-                    mOverlayStateController.setExitAnimationsRunning(false)
-                    doneCallback()
-                }
-                start()
-            }
-        mOverlayStateController.setExitAnimationsRunning(true)
+        executor.executeDelayed(doneCallback, DREAM_ANIMATION_DURATION.inWholeMilliseconds)
     }
 
     /** Cancels the dream content and dream overlay animations, if they're currently running. */
@@ -288,4 +281,15 @@
             mStatusBarViewController.setTranslationY(translationY)
         }
     }
+
+    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
+        return ConfigurationBasedDimensions(
+            translationYPx =
+                view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset),
+        )
+    }
+
+    private data class ConfigurationBasedDimensions(
+        val translationYPx: Int,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 9d7ad30..3106173 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -42,9 +42,9 @@
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.util.ViewController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.util.Arrays;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -170,6 +170,7 @@
     protected void onInit() {
         mStatusBarViewController.init();
         mComplicationHostViewController.init();
+        mDreamOverlayAnimationsController.init(mView);
     }
 
     @Override
@@ -184,7 +185,7 @@
 
         // Start dream entry animations. Skip animations for low light clock.
         if (!mStateController.isLowLightActive()) {
-            mDreamOverlayAnimationsController.startEntryAnimations(mView);
+            mDreamOverlayAnimationsController.startEntryAnimations();
         }
     }
 
@@ -261,10 +262,8 @@
      * @param onAnimationEnd Callback to trigger once animations are finished.
      * @param callbackExecutor Executor to execute the callback on.
      */
-    public void wakeUp(@NonNull Runnable onAnimationEnd, @NonNull Executor callbackExecutor) {
-        mDreamOverlayAnimationsController.startExitAnimations(mView, () -> {
-            callbackExecutor.execute(onAnimationEnd);
-            return null;
-        });
+    public void wakeUp(@NonNull Runnable onAnimationEnd,
+            @NonNull DelayableExecutor callbackExecutor) {
+        mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index e76d5b3..1be9cd1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -43,8 +43,7 @@
 import com.android.systemui.dreams.complication.Complication;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
-
-import java.util.concurrent.Executor;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -61,10 +60,11 @@
     // The Context is used to construct the hosting constraint layout and child overlay views.
     private final Context mContext;
     // The Executor ensures actions and ui updates happen on the same thread.
-    private final Executor mExecutor;
+    private final DelayableExecutor mExecutor;
     // A controller for the dream overlay container view (which contains both the status bar and the
     // content area).
     private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+    private final DreamCallbackController mDreamCallbackController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Nullable
     private final ComponentName mLowLightDreamComponent;
@@ -126,14 +126,15 @@
     @Inject
     public DreamOverlayService(
             Context context,
-            @Main Executor executor,
+            @Main DelayableExecutor executor,
             WindowManager windowManager,
             DreamOverlayComponent.Factory dreamOverlayComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             UiEventLogger uiEventLogger,
             @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
-                    ComponentName lowLightDreamComponent) {
+                    ComponentName lowLightDreamComponent,
+            DreamCallbackController dreamCallbackController) {
         mContext = context;
         mExecutor = executor;
         mWindowManager = windowManager;
@@ -142,6 +143,7 @@
         mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
         mStateController = stateController;
         mUiEventLogger = uiEventLogger;
+        mDreamCallbackController = dreamCallbackController;
 
         final ViewModelStore viewModelStore = new ViewModelStore();
         final Complication.Host host =
@@ -217,6 +219,7 @@
     public void onWakeUp(@NonNull Runnable onCompletedCallback) {
         mExecutor.execute(() -> {
             if (mDreamOverlayContainerViewController != null) {
+                mDreamCallbackController.onWakeUp();
                 mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 4f1ac1a..0d58a69 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -55,22 +55,6 @@
             "dream_in_complications_translation_y";
     public static final String DREAM_IN_TRANSLATION_Y_DURATION =
             "dream_in_complications_translation_y_duration";
-    public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE =
-            "dream_out_complications_translation_y";
-    public static final String DREAM_OUT_TRANSLATION_Y_DURATION =
-            "dream_out_complications_translation_y_duration";
-    public static final String DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM =
-            "dream_out_complications_translation_y_delay_bottom";
-    public static final String DREAM_OUT_TRANSLATION_Y_DELAY_TOP =
-            "dream_out_complications_translation_y_delay_top";
-    public static final String DREAM_OUT_ALPHA_DURATION =
-            "dream_out_complications_alpha_duration";
-    public static final String DREAM_OUT_ALPHA_DELAY_BOTTOM =
-            "dream_out_complications_alpha_delay_bottom";
-    public static final String DREAM_OUT_ALPHA_DELAY_TOP =
-            "dream_out_complications_alpha_delay_top";
-    public static final String DREAM_OUT_BLUR_DURATION =
-            "dream_out_blur_duration";
 
     /** */
     @Provides
@@ -185,66 +169,6 @@
         return (long) resources.getInteger(R.integer.config_dreamOverlayInTranslationYDurationMs);
     }
 
-    /**
-     * Provides the number of pixels to translate complications when waking up from dream.
-     */
-    @Provides
-    @Named(DREAM_OUT_TRANSLATION_Y_DISTANCE)
-    @DreamOverlayComponent.DreamOverlayScope
-    static int providesDreamOutComplicationsTranslationY(@Main Resources resources) {
-        return resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_TRANSLATION_Y_DURATION)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsTranslationYDuration(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDurationMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsTranslationYDelayBottom(@Main Resources resources) {
-        return (long) resources.getInteger(
-                R.integer.config_dreamOverlayOutTranslationYDelayBottomMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsTranslationYDelayTop(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDelayTopMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_ALPHA_DURATION)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsAlphaDuration(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDurationMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_ALPHA_DELAY_BOTTOM)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsAlphaDelayBottom(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayBottomMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_ALPHA_DELAY_TOP)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutComplicationsAlphaDelayTop(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayTopMs);
-    }
-
-    @Provides
-    @Named(DREAM_OUT_BLUR_DURATION)
-    @DreamOverlayComponent.DreamOverlayScope
-    static long providesDreamOutBlurDuration(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayOutBlurDurationMs);
-    }
-
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
     static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index c982131..276a290 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -51,6 +51,11 @@
         registerDumpable(name, module, DumpPriority.CRITICAL)
     }
 
+    /** See [registerNormalDumpable]. */
+    fun registerNormalDumpable(module: Dumpable) {
+        registerNormalDumpable(module::class.java.simpleName, module)
+    }
+
     /**
      * Registers a dumpable to be called during the NORMAL section of the bug report.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d231870..ae714fb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -33,6 +33,7 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.TO_LOCKSCREEN_DURATION_MS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -1231,8 +1232,7 @@
 
         mDreamOpenAnimationDuration = context.getResources().getInteger(
                 com.android.internal.R.integer.config_dreamOpenAnimationDuration);
-        mDreamCloseAnimationDuration = context.getResources().getInteger(
-                com.android.internal.R.integer.config_dreamCloseAnimationDuration);
+        mDreamCloseAnimationDuration = (int) TO_LOCKSCREEN_DURATION_MS;
     }
 
     public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 148792b..9a0fbbf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.doze.DozeMachine
 import com.android.systemui.doze.DozeTransitionCallback
 import com.android.systemui.doze.DozeTransitionListener
+import com.android.systemui.dreams.DreamCallbackController
+import com.android.systemui.dreams.DreamCallbackController.DreamCallback
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -47,6 +49,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.merge
 
 /** Defines interface for classes that encapsulate application state for the keyguard. */
 interface KeyguardRepository {
@@ -176,6 +179,7 @@
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val dozeTransitionListener: DozeTransitionListener,
     private val authController: AuthController,
+    private val dreamCallbackController: DreamCallbackController,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
     override val animateBottomAreaDozingTransitions =
@@ -276,22 +280,35 @@
             .distinctUntilChanged()
 
     override val isDreaming: Flow<Boolean> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : KeyguardUpdateMonitorCallback() {
-                        override fun onDreamingStateChanged(isDreaming: Boolean) {
-                            trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
+        merge(
+                conflatedCallbackFlow {
+                    val callback =
+                        object : KeyguardUpdateMonitorCallback() {
+                            override fun onDreamingStateChanged(isDreaming: Boolean) {
+                                trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
+                            }
                         }
-                    }
-                keyguardUpdateMonitor.registerCallback(callback)
-                trySendWithFailureLogging(
-                    keyguardUpdateMonitor.isDreaming,
-                    TAG,
-                    "initial isDreaming",
-                )
+                    keyguardUpdateMonitor.registerCallback(callback)
+                    trySendWithFailureLogging(
+                        keyguardUpdateMonitor.isDreaming,
+                        TAG,
+                        "initial isDreaming",
+                    )
 
-                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
-            }
+                    awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+                },
+                conflatedCallbackFlow {
+                    val callback =
+                        object : DreamCallback {
+                            override fun onWakeUp() {
+                                trySendWithFailureLogging(false, TAG, "updated isDreaming")
+                            }
+                        }
+                    dreamCallbackController.addCallback(callback)
+
+                    awaitClose { dreamCallbackController.removeCallback(callback) }
+                }
+            )
             .distinctUntilChanged()
 
     override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 5bb586e..d72d7183b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -66,8 +66,8 @@
     }
 
     /**
-     * Begin a transition from one state to another. Will not start if another transition is in
-     * progress.
+     * Begin a transition from one state to another. Transitions are interruptible, and will issue a
+     * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
      */
     fun startTransition(info: TransitionInfo): UUID?
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
index b73ce9e..4d60579 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
@@ -110,7 +112,7 @@
                                 name,
                                 KeyguardState.DREAMING,
                                 KeyguardState.LOCKSCREEN,
-                                getAnimator(),
+                                getAnimator(TO_LOCKSCREEN_DURATION),
                             )
                         )
                     }
@@ -167,14 +169,16 @@
         }
     }
 
-    private fun getAnimator(): ValueAnimator {
+    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
-            setDuration(TRANSITION_DURATION_MS)
+            setDuration(duration.inWholeMilliseconds)
         }
     }
 
     companion object {
-        private const val TRANSITION_DURATION_MS = 500L
+        private val DEFAULT_DURATION = 500.milliseconds
+        val TO_LOCKSCREEN_DURATION = 1183.milliseconds
+        @JvmField val TO_LOCKSCREEN_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 54a4f49..3b9d6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,12 +19,15 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.AnimationParams
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
+import kotlin.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -43,6 +46,10 @@
     /** LOCKSCREEN->AOD transition information. */
     val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
 
+    /** DREAMING->LOCKSCREEN transition information. */
+    val dreamingToLockscreenTransition: Flow<TransitionStep> =
+        repository.transition(DREAMING, LOCKSCREEN)
+
     /** (any)->AOD transition information */
     val anyStateToAodTransition: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.to == KeyguardState.AOD }
@@ -72,4 +79,21 @@
     /* The last completed [KeyguardState] transition */
     val finishedKeyguardState: Flow<KeyguardState> =
         finishedKeyguardTransitionStep.map { step -> step.to }
+
+    /**
+     * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
+     * range of [0, 1]. View animations should begin and end within a subset of this range. This
+     * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+     */
+    fun transitionStepAnimation(
+        flow: Flow<TransitionStep>,
+        params: AnimationParams,
+        totalDuration: Duration,
+    ): Flow<Float> {
+        val start = (params.startTime / totalDuration).toFloat()
+        val chunks = (totalDuration / params.duration).toFloat()
+        return flow
+            .map { step -> (step.value - start) * chunks }
+            .filter { value -> value >= 0f && value <= 1f }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
new file mode 100644
index 0000000..67733e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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.shared.model
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+/** Animation parameters */
+data class AnimationParams(
+    val startTime: Duration = 0.milliseconds,
+    val duration: Duration,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index 3d5985c5..f772b17 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -105,6 +105,14 @@
                     }
 
                     launch {
+                        viewModel.showWithFullExpansion.collect { model ->
+                            hostViewController.resetSecurityContainer()
+                            hostViewController.showPromptReason(model.promptReason)
+                            hostViewController.onResume()
+                        }
+                    }
+
+                    launch {
                         viewModel.hide.collect {
                             hostViewController.cancelDismissAction()
                             hostViewController.cleanUp()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..9b36e8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 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.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class DreamingToLockscreenTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+
+    /** Dream overlay y-translation on exit */
+    fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
+        return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
+            EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
+        }
+    }
+    /** Dream overlay views alpha - fade out */
+    val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+
+    /** Dream background alpha - fade out */
+    val dreamBackgroundAlpha: Flow<Float> = flowForAnimation(DREAM_BACKGROUND_ALPHA).map { 1f - it }
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { it * translatePx }
+    }
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
+
+    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+        return interactor.transitionStepAnimation(
+            interactor.dreamingToLockscreenTransition,
+            params,
+            totalDuration = TO_LOCKSCREEN_DURATION
+        )
+    }
+
+    companion object {
+        /* Length of time before ending the dream activity, in order to start unoccluding */
+        val DREAM_ANIMATION_DURATION = 250.milliseconds
+
+        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+        val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
+        val DREAM_BACKGROUND_ALPHA = AnimationParams(duration = 250.milliseconds)
+        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
+        val LOCKSCREEN_ALPHA =
+            AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 737c35d..e5d4e49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -22,8 +22,10 @@
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 
 /** Models UI state for the lock screen bouncer; handles user input. */
@@ -42,6 +44,10 @@
     /** Observe whether bouncer is showing. */
     val show: Flow<KeyguardBouncerModel> = interactor.show
 
+    /** Observe visible expansion when bouncer is showing. */
+    val showWithFullExpansion: Flow<KeyguardBouncerModel> =
+        interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
+
     /** Observe whether bouncer is hiding. */
     val hide: Flow<Unit> = interactor.hide
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index ab93b29..d6f941d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -58,10 +58,10 @@
 
     // Keep track of the key used for the session tokens. This information is used to know when to
     // dispatch a removed event so that a media object for a local session will be removed.
-    private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
+    private val keyedTokens: MutableMap<String, MutableSet<TokenId>> = mutableMapOf()
 
     // Keep track of which media session tokens have associated notifications.
-    private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
+    private val tokensWithNotifications: MutableSet<TokenId> = mutableSetOf()
 
     private val sessionListener =
         object : MediaSessionManager.OnActiveSessionsChangedListener {
@@ -101,15 +101,15 @@
         isSsReactivated: Boolean
     ) {
         backgroundExecutor.execute {
-            data.token?.let { tokensWithNotifications.add(it) }
+            data.token?.let { tokensWithNotifications.add(TokenId(it)) }
             val isMigration = oldKey != null && key != oldKey
             if (isMigration) {
                 keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
             }
             if (data.token != null) {
-                keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+                keyedTokens.get(key)?.let { tokens -> tokens.add(TokenId(data.token)) }
                     ?: run {
-                        val tokens = mutableSetOf(data.token)
+                        val tokens = mutableSetOf(TokenId(data.token))
                         keyedTokens.put(key, tokens)
                     }
             }
@@ -125,7 +125,7 @@
                 isMigration ||
                     remote == null ||
                     remote.sessionToken == data.token ||
-                    !tokensWithNotifications.contains(remote.sessionToken)
+                    !tokensWithNotifications.contains(TokenId(remote.sessionToken))
             ) {
                 // Not filtering in this case. Passing the event along to listeners.
                 dispatchMediaDataLoaded(key, oldKey, data, immediately)
@@ -136,7 +136,7 @@
                 // If the local session uses a different notification key, then lets go a step
                 // farther and dismiss the media data so that media controls for the local session
                 // don't hang around while casting.
-                if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+                if (!keyedTokens.get(key)!!.contains(TokenId(remote.sessionToken))) {
                     dispatchMediaDataRemoved(key)
                 }
             }
@@ -199,6 +199,15 @@
                     packageControllers.put(controller.packageName, tokens)
                 }
         }
-        tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
+        tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) })
+    }
+
+    /**
+     * Represents a unique identifier for a [MediaSession.Token].
+     *
+     * It's used to avoid storing strong binders for media session tokens.
+     */
+    private data class TokenId(val id: Int) {
+        constructor(token: MediaSession.Token) : this(token.hashCode())
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 7b9d0b4..358534c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.common.ui.binder.TintedIconViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttIcon
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
@@ -69,6 +70,7 @@
         mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
+        dumpManager: DumpManager,
         powerManager: PowerManager,
         @Main private val mainHandler: Handler,
         private val mediaTttFlags: MediaTttFlags,
@@ -83,6 +85,7 @@
         mainExecutor,
         accessibilityManager,
         configurationController,
+        dumpManager,
         powerManager,
         R.layout.media_ttt_chip_receiver,
         wakeLockBuilder,
@@ -162,6 +165,7 @@
     }
 
     override fun start() {
+        super.start()
         if (mediaTttFlags.isMediaTttEnabled()) {
             commandQueue.addCallback(commandQueueCallbacks)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 6db3c99..30f8124 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -237,7 +237,13 @@
             return
         }
 
-        buttonView.setBackgroundResource(model.background)
+        val backgroundResource =
+            when (model.backgroundColor) {
+                R.attr.offStateColor -> R.drawable.qs_footer_action_circle
+                com.android.internal.R.attr.colorAccent -> R.drawable.qs_footer_action_circle_color
+                else -> error("Unsupported icon background resource ${model.backgroundColor}")
+            }
+        buttonView.setBackgroundResource(backgroundResource)
         buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
 
         val icon = model.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 8d819da..2670787 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -16,7 +16,8 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.annotation.DrawableRes
+import android.annotation.AttrRes
+import android.annotation.ColorInt
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
@@ -27,7 +28,7 @@
 data class FooterActionsButtonViewModel(
     val id: Int,
     val icon: Icon,
-    val iconTint: Int?,
-    @DrawableRes val background: Int,
+    @ColorInt val iconTint: Int?,
+    @AttrRes val backgroundColor: Int,
     val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index dee6fad..fbf32b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.util.Log
+import android.view.ContextThemeWrapper
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -49,12 +50,15 @@
 
 /** A ViewModel for the footer actions. */
 class FooterActionsViewModel(
-    @Application private val context: Context,
+    @Application appContext: Context,
     private val footerActionsInteractor: FooterActionsInteractor,
     private val falsingManager: FalsingManager,
     private val globalActionsDialogLite: GlobalActionsDialogLite,
     showPowerButton: Boolean,
 ) {
+    /** The context themed with the Quick Settings colors. */
+    private val context = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)
+
     /**
      * Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
      * the UI should still participate to the layout it is included in (i.e. in the View world it
@@ -142,7 +146,7 @@
                 ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
             ),
             iconTint = null,
-            R.drawable.qs_footer_action_circle,
+            backgroundColor = R.attr.offStateColor,
             this::onSettingsButtonClicked,
         )
 
@@ -160,7 +164,7 @@
                         context,
                         com.android.internal.R.attr.textColorOnAccent,
                     ),
-                R.drawable.qs_footer_action_circle_color,
+                backgroundColor = com.android.internal.R.attr.colorAccent,
                 this::onPowerButtonClicked,
             )
         } else {
@@ -260,7 +264,7 @@
                     ),
                 ),
             iconTint = null,
-            background = R.drawable.qs_footer_action_circle,
+            backgroundColor = R.attr.offStateColor,
             onClick = this::onUserSwitcherClicked,
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 770a236..b7001e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -930,8 +930,7 @@
         if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
             if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
                 return; // udfps affordance is highlighted, no need to show action to unlock
-            } else if (!mKeyguardUpdateMonitor.getIsFaceAuthenticated()
-                    && mKeyguardUpdateMonitor.isFaceEnrolled()) {
+            } else if (mKeyguardUpdateMonitor.isFaceEnrolled()) {
                 String message = mContext.getString(R.string.keyguard_retry);
                 mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5db95d7..ca1e397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -146,7 +146,8 @@
     private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
     private boolean mShadeNeedsToClose = false;
 
-    private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+    @VisibleForTesting
+    static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
     private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
     private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
     /**
@@ -1326,8 +1327,11 @@
      * @param listenerNeedsAnimation does the listener need to animate?
      */
     private void updateStackPosition(boolean listenerNeedsAnimation) {
+        float topOverscrollAmount = mShouldUseSplitNotificationShade
+                ? getCurrentOverScrollAmount(true /* top */) : 0f;
         final float endTopPosition = mTopPadding + mExtraTopInsetForFullShadeTransition
                 + mAmbientState.getOverExpansion()
+                + topOverscrollAmount
                 - getCurrentOverScrollAmount(false /* top */);
         float fraction = mAmbientState.getExpansionFraction();
         // If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an
@@ -2613,8 +2617,10 @@
             float bottomAmount = getCurrentOverScrollAmount(false);
             if (velocityY < 0 && topAmount > 0) {
                 setOwnScrollY(mOwnScrollY - (int) topAmount);
-                mDontReportNextOverScroll = true;
-                setOverScrollAmount(0, true, false);
+                if (!mShouldUseSplitNotificationShade) {
+                    mDontReportNextOverScroll = true;
+                    setOverScrollAmount(0, true, false);
+                }
                 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
                         * mOverflingDistance + topAmount;
             } else if (velocityY > 0 && bottomAmount > 0) {
@@ -2648,6 +2654,7 @@
         float topOverScroll = getCurrentOverScrollAmount(true);
         return mScrolledToTopOnFirstDown
                 && !mExpandedInThisMotion
+                && !mShouldUseSplitNotificationShade
                 && (initialVelocity > mMinimumVelocity
                 || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
     }
@@ -2713,7 +2720,7 @@
             return RUBBER_BAND_FACTOR_AFTER_EXPAND;
         } else if (mIsExpansionChanging || mPanelTracking) {
             return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
-        } else if (mScrolledToTopOnFirstDown) {
+        } else if (mScrolledToTopOnFirstDown && !mShouldUseSplitNotificationShade) {
             return 1.0f;
         }
         return RUBBER_BAND_FACTOR_NORMAL;
@@ -5705,7 +5712,8 @@
         }
     }
 
-    private void updateSplitNotificationShade() {
+    @VisibleForTesting
+    void updateSplitNotificationShade() {
         boolean split = LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
         if (split != mShouldUseSplitNotificationShade) {
             mShouldUseSplitNotificationShade = split;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 1d00c33..a6b04e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -41,6 +41,7 @@
 data class MobileConnectionModel(
     /** From [ServiceStateListener.onServiceStateChanged] */
     val isEmergencyOnly: Boolean = false,
+    val isRoaming: Boolean = false,
 
     /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
     val isGsm: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 2621f997..fc59f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -50,4 +50,12 @@
      * [SubscriptionManager.getDefaultDataSubscriptionId]
      */
     val isDefaultDataSubscription: StateFlow<Boolean>
+
+    /**
+     * See [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber]. This bit only matters if
+     * the connection type is CDMA.
+     *
+     * True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF]
+     */
+    val cdmaRoaming: StateFlow<Boolean>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 1c08525..98b47e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -186,6 +186,7 @@
         connection.dataEnabled.value = true
         connection.isDefaultDataSubscription.value = state.dataType != null
 
+        connection.cdmaRoaming.value = state.roaming
         connection.connectionInfo.value = state.toMobileConnectionModel()
     }
 
@@ -229,6 +230,7 @@
     private fun Mobile.toMobileConnectionModel(): MobileConnectionModel {
         return MobileConnectionModel(
             isEmergencyOnly = false, // TODO(b/261029387): not yet supported
+            isRoaming = roaming,
             isGsm = false, // TODO(b/261029387): not yet supported
             cdmaLevel = level ?: 0,
             primaryLevel = level ?: 0,
@@ -260,4 +262,6 @@
     override val dataEnabled = MutableStateFlow(true)
 
     override val isDefaultDataSubscription = MutableStateFlow(true)
+
+    override val cdmaRoaming = MutableStateFlow(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
index da55787..2cdbc19 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -98,6 +98,7 @@
         val inflateStrength = getString("inflate")?.toBoolean()
         val activity = getString("activity")?.toActivity()
         val carrierNetworkChange = getString("carriernetworkchange") == "show"
+        val roaming = getString("roam") == "show"
 
         return Mobile(
             level = level,
@@ -107,6 +108,7 @@
             inflateStrength = inflateStrength,
             activity = activity,
             carrierNetworkChange = carrierNetworkChange,
+            roaming = roaming,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index 3f3acaf..b8543ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -34,6 +34,7 @@
         val inflateStrength: Boolean?,
         @DataActivityType val activity: Int?,
         val carrierNetworkChange: Boolean,
+        val roaming: Boolean,
     ) : FakeNetworkEventModel
 
     data class MobileDisabled(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 15505fd..295e0dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -27,6 +27,7 @@
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.ERI_OFF
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.qualifiers.Application
@@ -95,7 +96,11 @@
                         TelephonyCallback.CarrierNetworkListener,
                         TelephonyCallback.DisplayInfoListener {
                         override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            state =
+                                state.copy(
+                                    isEmergencyOnly = serviceState.isEmergencyOnly,
+                                    isRoaming = serviceState.roaming,
+                                )
                             trySend(state)
                         }
 
@@ -208,6 +213,11 @@
             globalMobileDataSettingChangedEvent,
         )
 
+    override val cdmaRoaming: StateFlow<Boolean> =
+        telephonyPollingEvent
+            .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     override val dataEnabled: StateFlow<Boolean> =
         telephonyPollingEvent
             .mapLatest { dataConnectionAllowed() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index a26f28a..15b70f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -54,6 +54,13 @@
     /** True if this line of service is emergency-only */
     val isEmergencyOnly: StateFlow<Boolean>
 
+    /**
+     * True if this connection is considered roaming. The roaming bit can come from [ServiceState],
+     * or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a
+     * connection to be roaming while carrier network change is active
+     */
+    val isRoaming: StateFlow<Boolean>
+
     /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
     val level: StateFlow<Int>
 
@@ -95,6 +102,18 @@
             .mapLatest { it.isEmergencyOnly }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
+    override val isRoaming: StateFlow<Boolean> =
+        combine(connectionInfo, connectionRepository.cdmaRoaming) { connection, cdmaRoaming ->
+                if (connection.carrierNetworkChangeActive) {
+                    false
+                } else if (connection.isGsm) {
+                    connection.isRoaming
+                } else {
+                    cdmaRoaming
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     override val level: StateFlow<Int> =
         connectionInfo
             .mapLatest { connection ->
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 67ea139..4455801 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
@@ -21,6 +21,7 @@
 import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
+import android.widget.Space
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
@@ -29,7 +30,6 @@
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
 
@@ -43,6 +43,8 @@
         val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
+        val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
+        val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
 
         view.isVisible = true
         iconView.isVisible = true
@@ -64,12 +66,21 @@
                     }
                 }
 
+                // Set the roaming indicator
+                launch {
+                    viewModel.roaming.distinctUntilChanged().collect { isRoaming ->
+                        roamingView.isVisible = isRoaming
+                        roamingSpace.isVisible = isRoaming
+                    }
+                }
+
                 // Set the tint
                 launch {
                     viewModel.tint.collect { tint ->
                         val tintList = ColorStateList.valueOf(tint)
                         iconView.imageTintList = tintList
                         networkTypeView.imageTintList = tintList
+                        roamingView.imageTintList = tintList
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 8ebd718..f4d6111 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -87,5 +87,7 @@
             }
         }
 
+    val roaming: Flow<Boolean> = iconInteractor.isRoaming
+
     val tint: Flow<Int> = flowOf(Color.CYAN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
index a4ca41c..9b41567 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
@@ -14,20 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.wifi.shared.model
+package com.android.systemui.statusbar.pipeline.shared.data.model
 
 import com.android.systemui.log.table.Diffable
 import com.android.systemui.log.table.TableRowLogger
 
-/** Provides information on the current wifi activity. */
-data class WifiActivityModel(
-    /** True if the wifi has activity in (download). */
+/** Provides information about the current data activity direction */
+data class DataActivityModel(
+    /** True if the connection has activity in (download). */
     val hasActivityIn: Boolean,
-    /** True if the wifi has activity out (upload). */
+    /** True if the connection has activity out (upload). */
     val hasActivityOut: Boolean,
-) : Diffable<WifiActivityModel> {
-
-    override fun logDiffs(prevVal: WifiActivityModel, row: TableRowLogger) {
+) : Diffable<DataActivityModel> {
+    override fun logDiffs(prevVal: DataActivityModel, row: TableRowLogger) {
         if (prevVal.hasActivityIn != hasActivityIn) {
             row.logChange(COL_ACTIVITY_IN, hasActivityIn)
         }
@@ -42,6 +41,6 @@
     }
 }
 
-const val ACTIVITY_PREFIX = "wifiActivity"
+const val ACTIVITY_PREFIX = "dataActivity"
 private const val COL_ACTIVITY_IN = "in"
 private const val COL_ACTIVITY_OUT = "out"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 0c9c1cc..8144198 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -42,9 +42,8 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.ACTIVITY_PREFIX
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -74,7 +73,7 @@
     val wifiNetwork: StateFlow<WifiNetworkModel>
 
     /** Observable for the current wifi network activity. */
-    val wifiActivity: StateFlow<WifiActivityModel>
+    val wifiActivity: StateFlow<DataActivityModel>
 }
 
 /** Real implementation of [WifiRepository]. */
@@ -230,7 +229,7 @@
             initialValue = WIFI_NETWORK_DEFAULT
         )
 
-    override val wifiActivity: StateFlow<WifiActivityModel> =
+    override val wifiActivity: StateFlow<DataActivityModel> =
             if (wifiManager == null) {
                 Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
                 flowOf(ACTIVITY_DEFAULT)
@@ -238,7 +237,7 @@
                 conflatedCallbackFlow {
                     val callback = TrafficStateCallback { state ->
                         logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
-                        trySend(trafficStateToWifiActivityModel(state))
+                        trySend(trafficStateToDataActivityModel(state))
                     }
                     wifiManager.registerTrafficStateCallback(mainExecutor, callback)
                     awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
@@ -256,7 +255,9 @@
                 )
 
     companion object {
-        val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        private const val ACTIVITY_PREFIX = "wifiActivity"
+
+        val ACTIVITY_DEFAULT = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
         // Start out with no known wifi network.
         // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an
         // initial fetch to get a starting wifi network. But, it uses a deprecated API
@@ -265,8 +266,8 @@
         // NetworkCallback inside [wifiNetwork] for our wifi network information.
         val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
 
-        private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
-            return WifiActivityModel(
+        private fun trafficStateToDataActivityModel(state: Int): DataActivityModel {
+            return DataActivityModel(
                 hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
                     state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
                 hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index ec935fe..93041ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -19,10 +19,10 @@
 import android.net.wifi.WifiManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
@@ -50,8 +50,8 @@
     /** Our current wifi network. See [WifiNetworkModel]. */
     val wifiNetwork: Flow<WifiNetworkModel>
 
-    /** Our current wifi activity. See [WifiActivityModel]. */
-    val activity: StateFlow<WifiActivityModel>
+    /** Our current wifi activity. See [DataActivityModel]. */
+    val activity: StateFlow<DataActivityModel>
 
     /** True if we're configured to force-hide the wifi icon and false otherwise. */
     val isForceHidden: Flow<Boolean>
@@ -82,7 +82,7 @@
 
     override val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
 
-    override val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity
+    override val activity: StateFlow<DataActivityModel> = wifiRepository.wifiActivity
 
     override val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
         it.contains(ConnectivitySlot.WIFI)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ec7ba65..07a7595 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -37,10 +37,10 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -147,7 +147,7 @@
             )
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
-    private val activity: Flow<WifiActivityModel?> =
+    private val activity: Flow<DataActivityModel?> =
         if (!wifiConstants.shouldShowActivityConfig) {
             flowOf(null)
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
new file mode 100644
index 0000000..154c6e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 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.stylus
+
+import android.content.Context
+import android.hardware.BatteryState
+import android.hardware.input.InputManager
+import android.os.Handler
+import android.util.Log
+import android.view.InputDevice
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * A listener that detects when a stylus has first been used, by detecting 1) the presence of an
+ * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth
+ * address.
+ */
+@SysUISingleton
+class StylusFirstUsageListener
+@Inject
+constructor(
+    private val context: Context,
+    private val inputManager: InputManager,
+    private val stylusManager: StylusManager,
+    private val featureFlags: FeatureFlags,
+    @Background private val executor: Executor,
+    @Background private val handler: Handler,
+) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
+
+    // Set must be only accessed from the background handler, which is the same handler that
+    // runs the StylusManager callbacks.
+    private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf()
+    @VisibleForTesting var hasStarted = false
+
+    override fun start() {
+        if (true) return // TODO(b/261826950): remove on main
+        if (hasStarted) return
+        if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
+        if (inputManager.isStylusEverUsed(context)) return
+        if (!hostDeviceSupportsStylusInput()) return
+
+        hasStarted = true
+        inputManager.inputDeviceIds.forEach(this::onStylusAdded)
+        stylusManager.registerCallback(this)
+        stylusManager.startListener()
+    }
+
+    override fun onStylusAdded(deviceId: Int) {
+        if (!hasStarted) return
+
+        val device = inputManager.getInputDevice(deviceId) ?: return
+        if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return
+
+        try {
+            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
+            internalStylusDeviceIds += deviceId
+        } catch (e: SecurityException) {
+            Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.")
+        }
+    }
+
+    override fun onStylusRemoved(deviceId: Int) {
+        if (!hasStarted) return
+
+        if (!internalStylusDeviceIds.contains(deviceId)) return
+        try {
+            inputManager.removeInputDeviceBatteryListener(deviceId, this)
+            internalStylusDeviceIds.remove(deviceId)
+        } catch (e: SecurityException) {
+            Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
+        }
+    }
+
+    override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
+        if (!hasStarted) return
+
+        onRemoteDeviceFound()
+    }
+
+    override fun onBatteryStateChanged(
+        deviceId: Int,
+        eventTimeMillis: Long,
+        batteryState: BatteryState
+    ) {
+        if (!hasStarted) return
+
+        if (batteryState.isPresent) {
+            onRemoteDeviceFound()
+        }
+    }
+
+    private fun onRemoteDeviceFound() {
+        inputManager.setStylusEverUsed(context, true)
+        cleanupListeners()
+    }
+
+    private fun cleanupListeners() {
+        stylusManager.unregisterCallback(this)
+        handler.post {
+            internalStylusDeviceIds.forEach {
+                inputManager.removeInputDeviceBatteryListener(it, this)
+            }
+        }
+    }
+
+    private fun hostDeviceSupportsStylusInput(): Boolean {
+        return inputManager.inputDeviceIds
+            .asSequence()
+            .mapNotNull { inputManager.getInputDevice(it) }
+            .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
+    }
+
+    companion object {
+        private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index db7315f..532fbaa 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -30,12 +30,16 @@
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
+import androidx.annotation.CallSuper
 import com.android.systemui.CoreStartable
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.wakelock.WakeLock
+import java.io.PrintWriter
 
 /**
  * A generic controller that can temporarily display a new view in a new window.
@@ -69,11 +73,12 @@
     @Main private val mainExecutor: DelayableExecutor,
     private val accessibilityManager: AccessibilityManager,
     private val configurationController: ConfigurationController,
+    private val dumpManager: DumpManager,
     private val powerManager: PowerManager,
     @LayoutRes private val viewLayoutRes: Int,
     private val wakeLockBuilder: WakeLock.Builder,
     private val systemClock: SystemClock,
-) : CoreStartable {
+) : CoreStartable, Dumpable {
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
      * all subclasses.
@@ -109,6 +114,11 @@
         return activeViews.getOrNull(0)
     }
 
+    @CallSuper
+    override fun start() {
+        dumpManager.registerNormalDumpable(this)
+    }
+
     /**
      * Displays the view with the provided [newInfo].
      *
@@ -382,6 +392,19 @@
         }
     }
 
+    @Synchronized
+    @CallSuper
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("Current time millis: ${systemClock.currentTimeMillis()}")
+        pw.println("Active views size: ${activeViews.size}")
+        activeViews.forEachIndexed { index, displayInfo ->
+            pw.println("View[$index]:")
+            pw.println("  info=${displayInfo.info}")
+            pw.println("  hasView=${displayInfo.view != null}")
+            pw.println("  timeExpiration=${displayInfo.timeExpirationMillis}")
+        }
+    }
+
     /**
      * A method implemented by subclasses to update [currentView] based on [newInfo].
      */
@@ -445,8 +468,6 @@
          */
         var cancelViewTimeout: Runnable?,
     )
-
-    // TODO(b/258019006): Add a dump method that dumps the currently active views.
 }
 
 private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index f37ef82..fd2c705 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.common.ui.binder.TintedIconViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -75,6 +76,7 @@
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
+        dumpManager: DumpManager,
         powerManager: PowerManager,
         private val falsingManager: FalsingManager,
         private val falsingCollector: FalsingCollector,
@@ -89,6 +91,7 @@
         mainExecutor,
         accessibilityManager,
         configurationController,
+        dumpManager,
         powerManager,
         R.layout.chipbar,
         wakeLockBuilder,
@@ -225,8 +228,6 @@
         return requireViewById(R.id.chipbar_inner)
     }
 
-    override fun start() {}
-
     override fun getTouchableRegion(view: View, outRect: Rect) {
         viewUtil.setRectToViewWindowLocation(view, outRect)
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index fa9bab2..1059543 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -150,10 +150,4 @@
                 getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
                 false);
     }
-
-    @Test
-    public void testReset() {
-        mKeyguardAbsKeyInputViewController.reset();
-        verify(mKeyguardMessageAreaController).setMessage("", false);
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
new file mode 100644
index 0000000..003efbf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 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.dreams
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamCallbackControllerTest : SysuiTestCase() {
+
+    @Mock private lateinit var callback: DreamCallbackController.DreamCallback
+
+    private lateinit var underTest: DreamCallbackController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = DreamCallbackController()
+    }
+
+    @Test
+    fun testOnWakeUpInvokesCallback() {
+        underTest.addCallback(callback)
+        underTest.onWakeUp()
+        verify(callback).onWakeUp()
+
+        // Adding twice should not invoke twice
+        reset(callback)
+        underTest.addCallback(callback)
+        underTest.onWakeUp()
+        verify(callback, times(1)).onWakeUp()
+
+        // After remove, no call to callback
+        reset(callback)
+        underTest.removeCallback(callback)
+        underTest.onWakeUp()
+        verify(callback, never()).onWakeUp()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 8e689cf..6c23254 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -1,18 +1,25 @@
 package com.android.systemui.dreams
 
-import android.animation.Animator
 import android.animation.AnimatorSet
 import android.testing.AndroidTestingRunner
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.statusbar.BlurUtils
-import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
@@ -28,14 +35,6 @@
         private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L
         private const val DREAM_IN_TRANSLATION_Y_DISTANCE = 6
         private const val DREAM_IN_TRANSLATION_Y_DURATION = 7L
-        private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6
-        private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L
-        private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L
-        private const val DREAM_OUT_TRANSLATION_Y_DELAY_TOP = 9L
-        private const val DREAM_OUT_ALPHA_DURATION = 10L
-        private const val DREAM_OUT_ALPHA_DELAY_BOTTOM = 11L
-        private const val DREAM_OUT_ALPHA_DELAY_TOP = 12L
-        private const val DREAM_OUT_BLUR_DURATION = 13L
     }
 
     @Mock private lateinit var mockAnimator: AnimatorSet
@@ -43,6 +42,8 @@
     @Mock private lateinit var hostViewController: ComplicationHostViewController
     @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController
     @Mock private lateinit var stateController: DreamOverlayStateController
+    @Mock private lateinit var configController: ConfigurationController
+    @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
     private lateinit var controller: DreamOverlayAnimationsController
 
     @Before
@@ -55,71 +56,48 @@
                 statusBarViewController,
                 stateController,
                 DREAM_BLUR_RADIUS,
+                transitionViewModel,
+                configController,
                 DREAM_IN_BLUR_ANIMATION_DURATION,
                 DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
                 DREAM_IN_TRANSLATION_Y_DISTANCE,
                 DREAM_IN_TRANSLATION_Y_DURATION,
-                DREAM_OUT_TRANSLATION_Y_DISTANCE,
-                DREAM_OUT_TRANSLATION_Y_DURATION,
-                DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM,
-                DREAM_OUT_TRANSLATION_Y_DELAY_TOP,
-                DREAM_OUT_ALPHA_DURATION,
-                DREAM_OUT_ALPHA_DELAY_BOTTOM,
-                DREAM_OUT_ALPHA_DELAY_TOP,
-                DREAM_OUT_BLUR_DURATION
             )
+
+        val mockView: View = mock()
+        whenever(mockView.resources).thenReturn(mContext.resources)
+
+        runBlocking(Dispatchers.Main.immediate) { controller.init(mockView) }
     }
 
     @Test
-    fun testExitAnimationOnEnd() {
-        val mockCallback: () -> Unit = mock()
+    fun testWakeUpCallsExecutor() {
+        val mockExecutor: DelayableExecutor = mock()
+        val mockCallback: Runnable = mock()
 
-        controller.startExitAnimations(
-            view = mock(),
+        controller.wakeUp(
             doneCallback = mockCallback,
-            animatorBuilder = { mockAnimator }
+            executor = mockExecutor,
         )
 
-        val captor = argumentCaptor<Animator.AnimatorListener>()
-        verify(mockAnimator).addListener(captor.capture())
-        val listener = captor.value
-
-        verify(mockCallback, never()).invoke()
-        listener.onAnimationEnd(mockAnimator)
-        verify(mockCallback, times(1)).invoke()
+        verify(mockExecutor).executeDelayed(eq(mockCallback), anyLong())
     }
 
     @Test
-    fun testCancellation() {
-        controller.startExitAnimations(
-            view = mock(),
-            doneCallback = mock(),
-            animatorBuilder = { mockAnimator }
-        )
-
-        verify(mockAnimator, never()).cancel()
-        controller.cancelAnimations()
-        verify(mockAnimator, times(1)).cancel()
-    }
-
-    @Test
-    fun testExitAfterStartWillCancel() {
+    fun testWakeUpAfterStartWillCancel() {
         val mockStartAnimator: AnimatorSet = mock()
-        val mockExitAnimator: AnimatorSet = mock()
 
-        controller.startEntryAnimations(view = mock(), animatorBuilder = { mockStartAnimator })
+        controller.startEntryAnimations(animatorBuilder = { mockStartAnimator })
 
         verify(mockStartAnimator, never()).cancel()
 
-        controller.startExitAnimations(
-            view = mock(),
+        controller.wakeUp(
             doneCallback = mock(),
-            animatorBuilder = { mockExitAnimator }
+            executor = mock(),
         )
 
         // Verify that we cancelled the start animator in favor of the exit
         // animator.
         verify(mockStartAnimator, times(1)).cancel()
-        verify(mockExitAnimator, never()).cancel()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 73c226d..2799a25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -203,7 +203,7 @@
 
         mController.onViewAttached();
 
-        verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView);
+        verify(mAnimationsController).startEntryAnimations();
         verify(mAnimationsController, never()).cancelAnimations();
     }
 
@@ -213,7 +213,7 @@
 
         mController.onViewAttached();
 
-        verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView);
+        verify(mAnimationsController, never()).startEntryAnimations();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index ffb8342..d6f8dea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -113,6 +113,9 @@
     @Mock
     UiEventLogger mUiEventLogger;
 
+    @Mock
+    DreamCallbackController mDreamCallbackController;
+
     @Captor
     ArgumentCaptor<View> mViewCaptor;
 
@@ -141,7 +144,8 @@
                 mStateController,
                 mKeyguardUpdateMonitor,
                 mUiEventLogger,
-                LOW_LIGHT_COMPONENT);
+                LOW_LIGHT_COMPONENT,
+                mDreamCallbackController);
     }
 
     @Test
@@ -353,6 +357,7 @@
         mService.onWakeUp(callback);
         mMainExecutor.runAllReady();
         verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor);
+        verify(mDreamCallbackController).onWakeUp();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 5deac19..563d44d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.doze.DozeMachine
 import com.android.systemui.doze.DozeTransitionCallback
 import com.android.systemui.doze.DozeTransitionListener
+import com.android.systemui.dreams.DreamCallbackController
+import com.android.systemui.dreams.DreamCallbackController.DreamCallback
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -66,6 +68,7 @@
     @Mock private lateinit var dozeTransitionListener: DozeTransitionListener
     @Mock private lateinit var authController: AuthController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var dreamCallbackController: DreamCallbackController
 
     private lateinit var underTest: KeyguardRepositoryImpl
 
@@ -83,6 +86,7 @@
                 keyguardUpdateMonitor,
                 dozeTransitionListener,
                 authController,
+                dreamCallbackController,
             )
     }
 
@@ -318,7 +322,7 @@
         }
 
     @Test
-    fun isDreaming() =
+    fun isDreamingFromKeyguardUpdateMonitor() =
         runTest(UnconfinedTestDispatcher()) {
             whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false)
             var latest: Boolean? = null
@@ -339,6 +343,26 @@
         }
 
     @Test
+    fun isDreamingFromDreamCallbackController() =
+        runTest(UnconfinedTestDispatcher()) {
+            whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true)
+            var latest: Boolean? = null
+            val job = underTest.isDreaming.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            val listener =
+                withArgCaptor<DreamCallbackController.DreamCallback> {
+                    verify(dreamCallbackController).addCallback(capture())
+                }
+
+            listener.onWakeUp()
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun biometricUnlockState() =
         runTest(UnconfinedTestDispatcher()) {
             val values = mutableListOf<BiometricUnlockModel>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..81950f1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: DreamingToLockscreenTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor = KeyguardTransitionInteractor(repository)
+        underTest = DreamingToLockscreenTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun dreamOverlayTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(1f))
+
+            // Only two values should be present, since the dream overlay runs for a small fraction
+            // of
+            // the overall animation time
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values[0])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
+                    ) * pixels
+                )
+            assertThat(values[1])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
+                    ) * pixels
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun dreamOverlayFadeOut() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(1f))
+
+            // Only two values should be present, since the dream overlay runs for a small fraction
+            // of
+            // the overall animation time
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
+            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+
+            job.cancel()
+        }
+
+    private fun animValue(stepValue: Float, params: AnimationParams): Float {
+        val totalDuration = TO_LOCKSCREEN_DURATION
+        val startValue = (params.startTime / totalDuration).toFloat()
+
+        val multiplier = (totalDuration / params.duration).toFloat()
+        return (stepValue - startValue) * multiplier
+    }
+
+    private fun step(value: Float): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = TransitionState.RUNNING,
+            ownerName = "DreamingToLockscreenTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index bad3f03..07a3109 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -22,6 +22,7 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
@@ -39,6 +40,7 @@
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
     configurationController: ConfigurationController,
+    dumpManager: DumpManager,
     powerManager: PowerManager,
     mainHandler: Handler,
     mediaTttFlags: MediaTttFlags,
@@ -55,6 +57,7 @@
         mainExecutor,
         accessibilityManager,
         configurationController,
+        dumpManager,
         powerManager,
         mainHandler,
         mediaTttFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index ef0bfb7..03ba3d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -34,6 +34,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
@@ -73,6 +74,8 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
     @Mock
+    private lateinit var dumpManager: DumpManager
+    @Mock
     private lateinit var mediaTttFlags: MediaTttFlags
     @Mock
     private lateinit var powerManager: PowerManager
@@ -122,6 +125,7 @@
             fakeExecutor,
             accessibilityManager,
             configurationController,
+            dumpManager,
             powerManager,
             Handler.getMain(),
             mediaTttFlags,
@@ -150,6 +154,7 @@
             FakeExecutor(FakeSystemClock()),
             accessibilityManager,
             configurationController,
+            dumpManager,
             powerManager,
             Handler.getMain(),
             mediaTttFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index b03a545..4cc12c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
@@ -81,6 +82,7 @@
     @Mock private lateinit var applicationInfo: ApplicationInfo
     @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var chipbarLogger: ChipbarLogger
@@ -137,6 +139,7 @@
                 fakeExecutor,
                 accessibilityManager,
                 configurationController,
+                dumpManager,
                 powerManager,
                 falsingManager,
                 falsingCollector,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 01411c9..0b9fbd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -84,7 +84,7 @@
                     ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
                 )
             )
-        assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
+        assertThat(settings.backgroundColor).isEqualTo(R.attr.offStateColor)
         assertThat(settings.iconTint).isNull()
     }
 
@@ -105,7 +105,7 @@
                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
                 )
             )
-        assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
+        assertThat(power.backgroundColor).isEqualTo(com.android.internal.R.attr.colorAccent)
         assertThat(power.iconTint)
             .isEqualTo(
                 Utils.getColorAttrDefaultColor(
@@ -170,7 +170,7 @@
         assertThat(userSwitcher).isNotNull()
         assertThat(userSwitcher!!.icon)
             .isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo")))
-        assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
+        assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.offStateColor)
 
         // Change the current user name.
         userSwitcherControllerWrapper.currentUserName = "bar"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 99b58a3..8d96932 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -660,7 +660,6 @@
         createController();
         String message = mContext.getString(R.string.keyguard_retry);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
-        when(mKeyguardUpdateMonitor.getIsFaceAuthenticated()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true);
 
         mController.setVisible(true);
@@ -671,21 +670,6 @@
     }
 
     @Test
-    public void transientIndication_swipeUpToRetry_faceAuthenticated() {
-        createController();
-        String message = mContext.getString(R.string.keyguard_retry);
-        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
-        when(mKeyguardUpdateMonitor.getIsFaceAuthenticated()).thenReturn(true);
-        when(mKeyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true);
-
-        mController.setVisible(true);
-        mController.getKeyguardCallback().onBiometricError(FACE_ERROR_TIMEOUT,
-                "A message", BiometricSourceType.FACE);
-
-        verify(mStatusBarKeyguardViewManager, never()).setKeyguardMessage(eq(message), any());
-    }
-
-    @Test
     public void faceErrorTimeout_whenFingerprintEnrolled_doesNotShowMessage() {
         createController();
         when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 07ea630..7622549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -20,6 +20,7 @@
 
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.RUBBER_BAND_FACTOR_NORMAL;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -47,6 +48,7 @@
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.testing.TestableResources;
 import android.util.MathUtils;
 import android.view.MotionEvent;
 import android.view.View;
@@ -99,6 +101,7 @@
     private NotificationStackScrollLayout mStackScroller;  // Normally test this
     private NotificationStackScrollLayout mStackScrollerInternal;  // See explanation below
     private AmbientState mAmbientState;
+    private TestableResources mTestableResources;
 
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
     @Mock private CentralSurfaces mCentralSurfaces;
@@ -123,6 +126,7 @@
     @UiThreadTest
     public void setUp() throws Exception {
         allowTestableLooperAsMainThread();
+        mTestableResources = mContext.getOrCreateTestableResources();
 
         // Interact with real instance of AmbientState.
         mAmbientState = spy(new AmbientState(
@@ -394,7 +398,7 @@
     @Test
     @UiThreadTest
     public void testSetExpandedHeight_withSplitShade_doesntInterpolateStackHeight() {
-        mContext.getOrCreateTestableResources()
+        mTestableResources
                 .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
         final int[] expectedStackHeight = {0};
 
@@ -405,12 +409,12 @@
                     .isEqualTo(expectedStackHeight[0]);
         });
 
-        mContext.getOrCreateTestableResources()
+        mTestableResources
                 .addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
         expectedStackHeight[0] = 0;
         mStackScroller.setExpandedHeight(100f);
 
-        mContext.getOrCreateTestableResources()
+        mTestableResources
                 .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
         expectedStackHeight[0] = 100;
         mStackScroller.setExpandedHeight(100f);
@@ -781,6 +785,39 @@
         assertEquals(mAmbientState.getScrollY(), 0);
     }
 
+    @Test
+    public void testSplitShade_hasTopOverscroll() {
+        mTestableResources
+                .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
+        mStackScroller.updateSplitNotificationShade();
+        mAmbientState.setExpansionFraction(1f);
+
+        int topOverscrollPixels = 100;
+        mStackScroller.setOverScrolledPixels(topOverscrollPixels, true, false);
+
+        float expectedTopOverscrollAmount = topOverscrollPixels * RUBBER_BAND_FACTOR_NORMAL;
+        assertEquals(expectedTopOverscrollAmount, mStackScroller.getCurrentOverScrollAmount(true));
+        assertEquals(expectedTopOverscrollAmount, mAmbientState.getStackY());
+    }
+
+    @Test
+    public void testNormalShade_hasNoTopOverscroll() {
+        mTestableResources
+                .addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
+        mStackScroller.updateSplitNotificationShade();
+        mAmbientState.setExpansionFraction(1f);
+
+        int topOverscrollPixels = 100;
+        mStackScroller.setOverScrolledPixels(topOverscrollPixels, true, false);
+
+        float expectedTopOverscrollAmount = topOverscrollPixels * RUBBER_BAND_FACTOR_NORMAL;
+        assertEquals(expectedTopOverscrollAmount, mStackScroller.getCurrentOverScrollAmount(true));
+        // When not in split shade mode, then the overscroll effect is handled in
+        // NotificationPanelViewController and not in NotificationStackScrollLayout. Therefore
+        // mAmbientState must have stackY set to 0
+        assertEquals(0f, mAmbientState.getStackY());
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 5265ec6..7b9929d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -30,6 +30,8 @@
     private val _isDefaultDataSubscription = MutableStateFlow(true)
     override val isDefaultDataSubscription = _isDefaultDataSubscription
 
+    override val cdmaRoaming = MutableStateFlow(false)
+
     fun setConnectionInfo(model: MobileConnectionModel) {
         _connectionInfo.value = model
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index e943de2..b2423da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -95,6 +95,7 @@
                     inflateStrength = testCase.inflateStrength,
                     activity = testCase.activity,
                     carrierNetworkChange = testCase.carrierNetworkChange,
+                    roaming = testCase.roaming,
                 )
 
             fakeNetworkEventFlow.value = networkModel
@@ -116,6 +117,7 @@
                 assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
+                assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -138,6 +140,7 @@
         val inflateStrength: Boolean,
         @Annotation.DataActivityType val activity: Int,
         val carrierNetworkChange: Boolean,
+        val roaming: Boolean,
     ) {
         override fun toString(): String {
             return "INPUT(level=$level, " +
@@ -146,7 +149,8 @@
                 "carrierId=$carrierId, " +
                 "inflateStrength=$inflateStrength, " +
                 "activity=$activity, " +
-                "carrierNetworkChange=$carrierNetworkChange)"
+                "carrierNetworkChange=$carrierNetworkChange, " +
+                "roaming=$roaming)"
         }
 
         // Convenience for iterating test data and creating new cases
@@ -158,6 +162,7 @@
             inflateStrength: Boolean? = null,
             @Annotation.DataActivityType activity: Int? = null,
             carrierNetworkChange: Boolean? = null,
+            roaming: Boolean? = null,
         ): TestCase =
             TestCase(
                 level = level ?: this.level,
@@ -166,7 +171,8 @@
                 carrierId = carrierId ?: this.carrierId,
                 inflateStrength = inflateStrength ?: this.inflateStrength,
                 activity = activity ?: this.activity,
-                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
+                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
+                roaming = roaming ?: this.roaming,
             )
     }
 
@@ -193,6 +199,8 @@
                 TelephonyManager.DATA_ACTIVITY_INOUT
             )
         private val carrierNetworkChange = booleanList
+        // false first so the base case doesn't have roaming set (more common)
+        private val roaming = listOf(false, true)
 
         @Parameters(name = "{0}") @JvmStatic fun data() = testData()
 
@@ -226,7 +234,8 @@
                     carrierIds.first(),
                     inflateStrength.first(),
                     activity.first(),
-                    carrierNetworkChange.first()
+                    carrierNetworkChange.first(),
+                    roaming.first(),
                 )
 
             val tail =
@@ -237,6 +246,7 @@
                         inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
                         activity.map { baseCase.modifiedBy(activity = it) },
                         carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
+                        roaming.map { baseCase.modifiedBy(roaming = it) }
                     )
                     .flatten()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index 32d0410..e4f29e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -292,6 +292,7 @@
                 assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
+                assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -313,6 +314,7 @@
     inflateStrength: Boolean? = false,
     activity: Int? = null,
     carrierNetworkChange: Boolean = false,
+    roaming: Boolean = false,
 ): FakeNetworkEventModel =
     FakeNetworkEventModel.Mobile(
         level = level,
@@ -322,4 +324,5 @@
         inflateStrength = inflateStrength,
         activity = activity,
         carrierNetworkChange = carrierNetworkChange,
+        roaming = roaming,
     )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 1fc9c60..0b3e5b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -32,6 +32,8 @@
 import android.telephony.TelephonyManager.DATA_DISCONNECTED
 import android.telephony.TelephonyManager.DATA_DISCONNECTING
 import android.telephony.TelephonyManager.DATA_UNKNOWN
+import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.ERI_ON
 import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
@@ -402,6 +404,61 @@
             job.cancel()
         }
 
+    @Test
+    fun `roaming - cdma - queries telephony manager`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            // Start the telephony collection job so that cdmaRoaming starts updating
+            val telephonyJob = underTest.connectionInfo.launchIn(this)
+            val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+            val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+            val serviceState = ServiceState()
+            serviceState.roaming = false
+
+            // CDMA roaming is off, GSM roaming is off
+            whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+            cb.onServiceStateChanged(serviceState)
+
+            assertThat(latest).isFalse()
+
+            // CDMA roaming is off, GSM roaming is on
+            whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
+            cb.onServiceStateChanged(serviceState)
+
+            assertThat(latest).isTrue()
+
+            telephonyJob.cancel()
+            job.cancel()
+        }
+
+    @Test
+    fun `roaming - gsm - queries service state`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.connectionInfo.onEach { latest = it.isRoaming }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.roaming = false
+
+            val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+            // CDMA roaming is off, GSM roaming is off
+            whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+            cb.onServiceStateChanged(serviceState)
+
+            assertThat(latest).isFalse()
+
+            // CDMA roaming is off, GSM roaming is on
+            serviceState.roaming = true
+            cb.onServiceStateChanged(serviceState)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
     private fun getTelephonyCallbacks(): List<TelephonyCallback> {
         val callbackCaptor = argumentCaptor<TelephonyCallback>()
         Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 1ff1636a..0e2c38e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -30,6 +30,8 @@
     private val _isEmergencyOnly = MutableStateFlow(false)
     override val isEmergencyOnly = _isEmergencyOnly
 
+    override val isRoaming = MutableStateFlow(false)
+
     private val _isFailedConnection = MutableStateFlow(false)
     override val isDefaultConnectionFailed = _isFailedConnection
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2281e89b..9b6f6df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -298,6 +298,100 @@
             job.cancel()
         }
 
+    @Test
+    fun `roaming - is gsm - uses connection model`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.cdmaRoaming.value = true
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = true,
+                    isRoaming = false,
+                )
+            )
+            yield()
+
+            assertThat(latest).isFalse()
+
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = true,
+                    isRoaming = true,
+                )
+            )
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `roaming - is cdma - uses cdma roaming bit`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.cdmaRoaming.value = false
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = false,
+                    isRoaming = true,
+                )
+            )
+            yield()
+
+            assertThat(latest).isFalse()
+
+            connectionRepository.cdmaRoaming.value = true
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = false,
+                    isRoaming = false,
+                )
+            )
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `roaming - false while carrierNetworkChangeActive`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.cdmaRoaming.value = true
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = false,
+                    isRoaming = true,
+                    carrierNetworkChangeActive = true,
+                )
+            )
+            yield()
+
+            assertThat(latest).isFalse()
+
+            connectionRepository.cdmaRoaming.value = true
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    isGsm = true,
+                    isRoaming = true,
+                    carrierNetworkChangeActive = true,
+                )
+            )
+            yield()
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index f2533a9..2c8f0a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -234,6 +234,22 @@
             job.cancel()
         }
 
+    @Test
+    fun roaming() =
+        runBlocking(IMMEDIATE) {
+            interactor.isRoaming.value = true
+            var latest: Boolean? = null
+            val job = underTest.roaming.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            interactor.isRoaming.value = false
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     /** Convenience constructor for these tests */
     private fun defaultSignal(
         level: Int = 1,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index 2f18ce3..4e15b4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -35,7 +35,7 @@
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
 
     private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
-    override val wifiActivity: StateFlow<WifiActivityModel> = _wifiActivity
+    override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity
 
     fun setIsWifiEnabled(enabled: Boolean) {
         _isWifiEnabled.value = enabled
@@ -49,7 +49,7 @@
         _wifiNetwork.value = wifiNetworkModel
     }
 
-    fun setWifiActivity(activity: WifiActivityModel) {
+    fun setWifiActivity(activity: DataActivityModel) {
         _wifiActivity.value = activity
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 800f3c0..5d0d87b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -31,10 +31,10 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -724,7 +724,7 @@
     fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
         underTest = createRepo(wifiManagerToUse = null)
 
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
                 .wifiActivity
                 .onEach { latest = it }
@@ -737,7 +737,7 @@
 
     @Test
     fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
                 .wifiActivity
                 .onEach { latest = it }
@@ -746,7 +746,7 @@
         getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
 
         assertThat(latest).isEqualTo(
-            WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+            DataActivityModel(hasActivityIn = false, hasActivityOut = false)
         )
 
         job.cancel()
@@ -754,7 +754,7 @@
 
     @Test
     fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
                 .wifiActivity
                 .onEach { latest = it }
@@ -763,7 +763,7 @@
         getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
 
         assertThat(latest).isEqualTo(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+            DataActivityModel(hasActivityIn = true, hasActivityOut = false)
         )
 
         job.cancel()
@@ -771,7 +771,7 @@
 
     @Test
     fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
                 .wifiActivity
                 .onEach { latest = it }
@@ -780,7 +780,7 @@
         getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
 
         assertThat(latest).isEqualTo(
-            WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+            DataActivityModel(hasActivityIn = false, hasActivityOut = true)
         )
 
         job.cancel()
@@ -788,7 +788,7 @@
 
     @Test
     fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
                 .wifiActivity
                 .onEach { latest = it }
@@ -796,7 +796,7 @@
 
         getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
 
-        assertThat(latest).isEqualTo(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+        assertThat(latest).isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
 
         job.cancel()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index b38497a..2ecb17b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -20,10 +20,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -225,23 +225,23 @@
 
     @Test
     fun activity_matchesRepoWifiActivity() = runBlocking(IMMEDIATE) {
-        var latest: WifiActivityModel? = null
+        var latest: DataActivityModel? = null
         val job = underTest
             .activity
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity1 = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+        val activity1 = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity1)
         yield()
         assertThat(latest).isEqualTo(activity1)
 
-        val activity2 = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        val activity2 = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity2)
         yield()
         assertThat(latest).isEqualTo(activity2)
 
-        val activity3 = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        val activity3 = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity3)
         yield()
         assertThat(latest).isEqualTo(activity3)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 7502020..b47f177 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -27,13 +27,13 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -209,7 +209,7 @@
             .launchIn(this)
 
         // WHEN we update the repo to have activity
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -252,7 +252,7 @@
             .launchIn(this)
 
         // WHEN we update the repo to have activity
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -293,7 +293,7 @@
             .onEach { latestQs = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -319,7 +319,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -341,7 +341,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -363,7 +363,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -385,7 +385,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -407,7 +407,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -429,7 +429,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -451,7 +451,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
         wifiRepository.setWifiActivity(activity)
         yield()
 
@@ -473,7 +473,7 @@
             .onEach { latest = it }
             .launchIn(this)
 
-        val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
         wifiRepository.setWifiActivity(activity)
         yield()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
new file mode 100644
index 0000000..8dd088f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2022 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.stylus
+
+import android.content.Context
+import android.hardware.BatteryState
+import android.hardware.input.InputManager
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.view.InputDevice
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@Ignore("TODO(b/20579491): unignore on main")
+class StylusFirstUsageListenerTest : SysuiTestCase() {
+    @Mock lateinit var context: Context
+    @Mock lateinit var inputManager: InputManager
+    @Mock lateinit var stylusManager: StylusManager
+    @Mock lateinit var featureFlags: FeatureFlags
+    @Mock lateinit var internalStylusDevice: InputDevice
+    @Mock lateinit var otherDevice: InputDevice
+    @Mock lateinit var externalStylusDevice: InputDevice
+    @Mock lateinit var batteryState: BatteryState
+    @Mock lateinit var handler: Handler
+
+    private lateinit var stylusListener: StylusFirstUsageListener
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
+        whenever(inputManager.isStylusEverUsed(context)).thenReturn(false)
+
+        stylusListener =
+            StylusFirstUsageListener(
+                context,
+                inputManager,
+                stylusManager,
+                featureFlags,
+                EXECUTOR,
+                handler
+            )
+        stylusListener.hasStarted = false
+
+        whenever(handler.post(any())).thenAnswer {
+            (it.arguments[0] as Runnable).run()
+            true
+        }
+
+        whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
+        whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
+        whenever(internalStylusDevice.isExternal).thenReturn(false)
+        whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
+        whenever(externalStylusDevice.isExternal).thenReturn(true)
+
+        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
+        whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
+        whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID))
+            .thenReturn(internalStylusDevice)
+        whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID))
+            .thenReturn(externalStylusDevice)
+    }
+
+    @Test
+    fun start_flagDisabled_doesNotRegister() {
+        whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false)
+
+        stylusListener.start()
+
+        verify(stylusManager, never()).registerCallback(any())
+        verify(inputManager, never()).setStylusEverUsed(context, true)
+    }
+
+    @Test
+    fun start_toggleHasStarted() {
+        stylusListener.start()
+
+        assert(stylusListener.hasStarted)
+    }
+
+    @Test
+    fun start_hasStarted_doesNotRegister() {
+        stylusListener.hasStarted = true
+
+        stylusListener.start()
+
+        verify(stylusManager, never()).registerCallback(any())
+    }
+
+    @Test
+    fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
+        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID))
+
+        stylusListener.start()
+
+        verify(stylusManager, never()).registerCallback(any())
+        verify(inputManager, never()).setStylusEverUsed(context, true)
+    }
+
+    @Test
+    fun start_stylusEverUsed_doesNotRegister() {
+        whenever(inputManager.inputDeviceIds)
+            .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
+        whenever(inputManager.isStylusEverUsed(context)).thenReturn(true)
+
+        stylusListener.start()
+
+        verify(stylusManager, never()).registerCallback(any())
+        verify(inputManager, never()).setStylusEverUsed(context, true)
+    }
+
+    @Test
+    fun start_hostDeviceSupportsStylus_registersListener() {
+        whenever(inputManager.inputDeviceIds)
+            .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
+
+        stylusListener.start()
+
+        verify(stylusManager).registerCallback(any())
+        verify(inputManager, never()).setStylusEverUsed(context, true)
+    }
+
+    @Test
+    fun onStylusAdded_hasNotStarted_doesNotRegisterListener() {
+        stylusListener.hasStarted = false
+
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        verifyZeroInteractions(inputManager)
+    }
+
+    @Test
+    fun onStylusAdded_internalStylus_registersListener() {
+        stylusListener.hasStarted = true
+
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        verify(inputManager, times(1))
+            .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener)
+    }
+
+    @Test
+    fun onStylusAdded_externalStylus_doesNotRegisterListener() {
+        stylusListener.hasStarted = true
+
+        stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID)
+
+        verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
+    }
+
+    @Test
+    fun onStylusAdded_otherDevice_doesNotRegisterListener() {
+        stylusListener.onStylusAdded(OTHER_DEVICE_ID)
+
+        verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
+    }
+
+    @Test
+    fun onStylusRemoved_registeredDevice_unregistersListener() {
+        stylusListener.hasStarted = true
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
+
+        verify(inputManager, times(1))
+            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
+    }
+
+    @Test
+    fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() {
+        stylusListener.hasStarted = false
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
+
+        verifyZeroInteractions(inputManager)
+    }
+
+    @Test
+    fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() {
+        stylusListener.hasStarted = true
+
+        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
+
+        verifyNoMoreInteractions(inputManager)
+    }
+
+    @Test
+    fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() {
+        stylusListener.hasStarted = true
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
+
+        verify(inputManager).setStylusEverUsed(context, true)
+        verify(inputManager, times(1))
+            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
+        verify(stylusManager).unregisterCallback(stylusListener)
+    }
+
+    @Test
+    fun onStylusBluetoothConnected_hasNotStarted_doesNoting() {
+        stylusListener.hasStarted = false
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+
+        stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
+
+        verifyZeroInteractions(inputManager)
+        verifyZeroInteractions(stylusManager)
+    }
+
+    @Test
+    fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() {
+        stylusListener.hasStarted = true
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+        whenever(batteryState.isPresent).thenReturn(true)
+
+        stylusListener.onBatteryStateChanged(0, 1, batteryState)
+
+        verify(inputManager).setStylusEverUsed(context, true)
+        verify(inputManager, times(1))
+            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
+        verify(stylusManager).unregisterCallback(stylusListener)
+    }
+
+    @Test
+    fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() {
+        stylusListener.hasStarted = true
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+        whenever(batteryState.isPresent).thenReturn(false)
+
+        stylusListener.onBatteryStateChanged(0, 1, batteryState)
+
+        verifyZeroInteractions(stylusManager)
+        verify(inputManager, never())
+            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
+    }
+
+    @Test
+    fun onBatteryStateChanged_hasNotStarted_doesNothing() {
+        stylusListener.hasStarted = false
+        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
+        whenever(batteryState.isPresent).thenReturn(false)
+
+        stylusListener.onBatteryStateChanged(0, 1, batteryState)
+
+        verifyZeroInteractions(inputManager)
+        verifyZeroInteractions(stylusManager)
+    }
+
+    companion object {
+        private const val OTHER_DEVICE_ID = 0
+        private const val INTERNAL_STYLUS_DEVICE_ID = 1
+        private const val EXTERNAL_STYLUS_DEVICE_ID = 2
+        private val EXECUTOR = FakeExecutor(FakeSystemClock())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 82153d5..99e2012 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -66,6 +67,8 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
     @Mock
+    private lateinit var dumpManager: DumpManager
+    @Mock
     private lateinit var windowManager: WindowManager
     @Mock
     private lateinit var powerManager: PowerManager
@@ -91,6 +94,7 @@
             fakeExecutor,
             accessibilityManager,
             configurationController,
+            dumpManager,
             powerManager,
             fakeWakeLockBuilder,
             fakeClock,
@@ -989,6 +993,7 @@
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
+        dumpManager: DumpManager,
         powerManager: PowerManager,
         wakeLockBuilder: WakeLock.Builder,
         systemClock: SystemClock,
@@ -999,6 +1004,7 @@
         mainExecutor,
         accessibilityManager,
         configurationController,
+        dumpManager,
         powerManager,
         R.layout.chipbar,
         wakeLockBuilder,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 2e4d8e7..d3411c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.TintedIcon
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -66,6 +67,7 @@
     @Mock private lateinit var logger: ChipbarLogger
     @Mock private lateinit var accessibilityManager: AccessibilityManager
     @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var windowManager: WindowManager
     @Mock private lateinit var falsingManager: FalsingManager
@@ -100,6 +102,7 @@
                 fakeExecutor,
                 accessibilityManager,
                 configurationController,
+                dumpManager,
                 powerManager,
                 falsingManager,
                 falsingCollector,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index d5167b3..b9a5bd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -22,6 +22,7 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -38,6 +39,7 @@
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
     configurationController: ConfigurationController,
+    dumpManager: DumpManager,
     powerManager: PowerManager,
     falsingManager: FalsingManager,
     falsingCollector: FalsingCollector,
@@ -53,6 +55,7 @@
         mainExecutor,
         accessibilityManager,
         configurationController,
+        dumpManager,
         powerManager,
         falsingManager,
         falsingCollector,
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 401d184..5bdfa70 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -46,6 +46,7 @@
 import com.android.server.display.config.DisplayQuirks;
 import com.android.server.display.config.HbmTiming;
 import com.android.server.display.config.HighBrightnessMode;
+import com.android.server.display.config.IntegerArray;
 import com.android.server.display.config.NitsMap;
 import com.android.server.display.config.Point;
 import com.android.server.display.config.RefreshRateConfigs;
@@ -213,6 +214,10 @@
  *        <type>android.sensor.light</type>
  *        <name>1234 Ambient Light Sensor</name>
  *      </lightSensor>
+ *      <screenOffBrightnessSensor>
+ *        <type>com.google.sensor.binned_brightness</type>
+ *        <name>Binned Brightness 0 (wake-up)</name>
+ *      </screenOffBrightnessSensor>
  *      <proxSensor>
  *        <type>android.sensor.proximity</type>
  *        <name>1234 Proximity Sensor</name>
@@ -368,6 +373,13 @@
  *             </brightnessThresholdPoints>
  *         </darkeningThresholds>
  *     </displayBrightnessChangeThresholdsIdle>
+ *     <screenOffBrightnessSensorValueToLux>
+ *         <item>-1</item>
+ *         <item>0</item>
+ *         <item>5</item>
+ *         <item>80</item>
+ *         <item>1500</item>
+ *     </screenOffBrightnessSensorValueToLux>
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -428,6 +440,9 @@
     // The details of the ambient light sensor associated with this display.
     private final SensorData mAmbientLightSensor = new SensorData();
 
+    // The details of the doze brightness sensor associated with this display.
+    private final SensorData mScreenOffBrightnessSensor = new SensorData();
+
     // The details of the proximity sensor associated with this display.
     private final SensorData mProximitySensor = new SensorData();
 
@@ -523,6 +538,9 @@
     private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
     private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
 
+    // A mapping between screen off sensor values and lux values
+    private int[] mScreenOffBrightnessSensorValueToLux;
+
     private Spline mBrightnessToBacklightSpline;
     private Spline mBacklightToBrightnessSpline;
     private Spline mBacklightToNitsSpline;
@@ -1198,6 +1216,10 @@
         return mAmbientLightSensor;
     }
 
+    SensorData getScreenOffBrightnessSensor() {
+        return mScreenOffBrightnessSensor;
+    }
+
     SensorData getProximitySensor() {
         return mProximitySensor;
     }
@@ -1321,6 +1343,14 @@
         return mHighAmbientBrightnessThresholds;
     }
 
+    /**
+     * @return A mapping from screen off brightness sensor readings to lux values. This estimates
+     * the ambient lux when the screen is off to determine the initial brightness
+     */
+    public int[] getScreenOffBrightnessSensorValueToLux() {
+        return mScreenOffBrightnessSensorValueToLux;
+    }
+
     @Override
     public String toString() {
         return "DisplayDeviceConfig{"
@@ -1399,6 +1429,7 @@
                 mScreenDarkeningPercentagesIdle)
                 + "\n"
                 + ", mAmbientLightSensor=" + mAmbientLightSensor
+                + ", mScreenOffBrightnessSensor=" + mScreenOffBrightnessSensor
                 + ", mProximitySensor=" + mProximitySensor
                 + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
                 + ", mDensityMapping= " + mDensityMapping
@@ -1421,6 +1452,9 @@
                 + Arrays.toString(mHighDisplayBrightnessThresholds)
                 + ", mHighAmbientBrightnessThresholds= "
                 + Arrays.toString(mHighAmbientBrightnessThresholds)
+                + "\n"
+                + ", mScreenOffBrightnessSensorValueToLux=" + Arrays.toString(
+                mScreenOffBrightnessSensorValueToLux)
                 + "}";
     }
 
@@ -1474,11 +1508,13 @@
                 loadQuirks(config);
                 loadBrightnessRamps(config);
                 loadAmbientLightSensorFromDdc(config);
+                loadScreenOffBrightnessSensorFromDdc(config);
                 loadProxSensorFromDdc(config);
                 loadAmbientHorizonFromDdc(config);
                 loadBrightnessChangeThresholds(config);
                 loadAutoBrightnessConfigValues(config);
                 loadRefreshRateSetting(config);
+                loadScreenOffBrightnessSensorValueToLuxFromDdc(config);
             } else {
                 Slog.w(TAG, "DisplayDeviceConfig file is null");
             }
@@ -2175,6 +2211,14 @@
         mProximitySensor.type = "";
     }
 
+    private void loadScreenOffBrightnessSensorFromDdc(DisplayConfiguration config) {
+        final SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
+        if (sensorDetails != null) {
+            mScreenOffBrightnessSensor.type = sensorDetails.getType();
+            mScreenOffBrightnessSensor.name = sensorDetails.getName();
+        }
+    }
+
     private void loadProxSensorFromDdc(DisplayConfiguration config) {
         SensorDetails sensorDetails = config.getProxSensor();
         if (sensorDetails != null) {
@@ -2587,6 +2631,19 @@
                 && mDdcAutoBrightnessAvailable;
     }
 
+    private void loadScreenOffBrightnessSensorValueToLuxFromDdc(DisplayConfiguration config) {
+        IntegerArray sensorValueToLux = config.getScreenOffBrightnessSensorValueToLux();
+        if (sensorValueToLux == null) {
+            return;
+        }
+
+        List<BigInteger> items = sensorValueToLux.getItem();
+        mScreenOffBrightnessSensorValueToLux = new int[items.size()];
+        for (int i = 0; i < items.size(); i++) {
+            mScreenOffBrightnessSensorValueToLux[i] = items.get(i).intValue();
+        }
+    }
+
     static class SensorData {
         public String type;
         public String name;
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index c740b98..d791c06 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -414,7 +414,12 @@
     @Nullable
     private AutomaticBrightnessController mAutomaticBrightnessController;
 
+    // The controller for the sensor used to estimate ambient lux while the display is off.
+    @Nullable
+    private ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
+
     private Sensor mLightSensor;
+    private Sensor mScreenOffBrightnessSensor;
 
     // The mappers between ambient lux, display backlight values, and display brightness.
     // We will switch between the idle mapper and active mapper in AutomaticBrightnessController.
@@ -1098,6 +1103,19 @@
 
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
+
+            loadScreenOffBrightnessSensor();
+            int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux();
+            if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) {
+                mScreenOffBrightnessSensorController = new ScreenOffBrightnessSensorController(
+                        mSensorManager,
+                        mScreenOffBrightnessSensor,
+                        mHandler,
+                        SystemClock::uptimeMillis,
+                        sensorValueToLux,
+                        mInteractiveModeBrightnessMapper
+                );
+            }
         } else {
             mUseSoftwareAutoBrightnessConfig = false;
         }
@@ -1275,6 +1293,12 @@
         }
         assert(state != Display.STATE_UNKNOWN);
 
+        if (mScreenOffBrightnessSensorController != null) {
+            mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness
+                    && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
+                    && !mAllowAutoBrightnessWhileDozingConfig)));
+        }
+
         boolean skipRampBecauseOfProximityChangeToNegative = false;
         // Apply the proximity sensor.
         if (mProximitySensor != null) {
@@ -1454,6 +1478,9 @@
                 updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
                 mAppliedAutoBrightness = true;
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
+                if (mScreenOffBrightnessSensorController != null) {
+                    mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
+                }
             } else {
                 mAppliedAutoBrightness = false;
             }
@@ -1481,6 +1508,19 @@
             mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
         }
 
+        // The ALS is not available yet - use the screen off sensor to determine the initial
+        // brightness
+        if (Float.isNaN(brightnessState) && autoBrightnessEnabled
+                && mScreenOffBrightnessSensorController != null) {
+            brightnessState = mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
+            if (isValidBrightnessValue(brightnessState)) {
+                brightnessState = clampScreenBrightness(brightnessState);
+                updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
+                mBrightnessReasonTemp.setReason(
+                        BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
+            }
+        }
+
         // Apply manual brightness.
         if (Float.isNaN(brightnessState)) {
             brightnessState = clampScreenBrightness(mCurrentScreenBrightnessSetting);
@@ -2052,6 +2092,14 @@
                 fallbackType);
     }
 
+    private void loadScreenOffBrightnessSensor() {
+        DisplayDeviceConfig.SensorData screenOffBrightnessSensor =
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor();
+        mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
+                screenOffBrightnessSensor.type, screenOffBrightnessSensor.name,
+                SensorUtils.NO_FALLBACK);
+    }
+
     private void loadProximitySensor() {
         if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
             return;
@@ -3201,7 +3249,8 @@
         static final int REASON_OVERRIDE = 7;
         static final int REASON_TEMPORARY = 8;
         static final int REASON_BOOST = 9;
-        static final int REASON_MAX = REASON_BOOST;
+        static final int REASON_SCREEN_OFF_BRIGHTNESS_SENSOR = 10;
+        static final int REASON_MAX = REASON_SCREEN_OFF_BRIGHTNESS_SENSOR;
 
         static final int MODIFIER_DIMMED = 0x1;
         static final int MODIFIER_LOW_POWER = 0x2;
@@ -3311,6 +3360,7 @@
                 case REASON_OVERRIDE: return "override";
                 case REASON_TEMPORARY: return "temporary";
                 case REASON_BOOST: return "boost";
+                case REASON_SCREEN_OFF_BRIGHTNESS_SENSOR: return "screen_off_brightness_sensor";
                 default: return Integer.toString(reason);
             }
         }
diff --git a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
new file mode 100644
index 0000000..6f50dac
--- /dev/null
+++ b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 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.server.display;
+
+import android.annotation.Nullable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * Controls the light sensor when the screen is off. The sensor used here does not report lux values
+ * but an index that needs to be mapped to a lux value.
+ */
+public class ScreenOffBrightnessSensorController implements SensorEventListener {
+    private static final String TAG = "ScreenOffBrightnessSensorController";
+
+    private static final int SENSOR_INVALID_VALUE = -1;
+    private static final long SENSOR_VALUE_VALID_TIME_MILLIS = 1500;
+
+    private final Handler mHandler;
+    private final Clock mClock;
+    private final SensorManager mSensorManager;
+    private final Sensor mLightSensor;
+    private final int[] mSensorValueToLux;
+
+    private boolean mRegistered;
+    private int mLastSensorValue = SENSOR_INVALID_VALUE;
+    private long mSensorDisableTime = -1;
+
+    // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
+    @Nullable
+    private final BrightnessMappingStrategy mBrightnessMapper;
+
+    public ScreenOffBrightnessSensorController(
+            SensorManager sensorManager,
+            Sensor lightSensor,
+            Handler handler,
+            Clock clock,
+            int[] sensorValueToLux,
+            BrightnessMappingStrategy brightnessMapper) {
+        mSensorManager = sensorManager;
+        mLightSensor = lightSensor;
+        mHandler = handler;
+        mClock = clock;
+        mSensorValueToLux = sensorValueToLux;
+        mBrightnessMapper = brightnessMapper;
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        if (mRegistered) {
+            mLastSensorValue = (int) event.values[0];
+        }
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    void setLightSensorEnabled(boolean enabled) {
+        if (enabled && !mRegistered) {
+            // Wait until we get an event from the sensor indicating ready.
+            mRegistered = mSensorManager.registerListener(this, mLightSensor,
+                    SensorManager.SENSOR_DELAY_NORMAL, mHandler);
+            mLastSensorValue = SENSOR_INVALID_VALUE;
+        } else if (!enabled && mRegistered) {
+            mSensorManager.unregisterListener(this);
+            mRegistered = false;
+            mSensorDisableTime = mClock.uptimeMillis();
+        }
+    }
+
+    float getAutomaticScreenBrightness() {
+        if (mLastSensorValue < 0 || mLastSensorValue >= mSensorValueToLux.length
+                || (!mRegistered
+                && mClock.uptimeMillis() - mSensorDisableTime > SENSOR_VALUE_VALID_TIME_MILLIS)) {
+            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+        }
+
+        int lux = mSensorValueToLux[mLastSensorValue];
+        if (lux < 0) {
+            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+        }
+
+        return mBrightnessMapper.getBrightness(lux);
+    }
+
+    /** Dump current state */
+    public void dump(PrintWriter pw) {
+        pw.println("ScreenOffBrightnessSensorController:");
+        IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
+        idpw.increaseIndent();
+        idpw.println("registered=" + mRegistered);
+        idpw.println("lastSensorValue=" + mLastSensorValue);
+    }
+
+    /** Functional interface for providing time. */
+    @VisibleForTesting
+    interface Clock {
+        /**
+         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
+         */
+        long uptimeMillis();
+    }
+}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index dbe049a..de0d1f8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1049,13 +1049,6 @@
             return;
         }
 
-        // Don't dream if the user isn't user zero.
-        // TODO(b/261907079): Move this check to DreamManagerService#canStartDreamingInternal().
-        if (ActivityManager.getCurrentUser() != UserHandle.USER_SYSTEM) {
-            noDreamAction.run();
-            return;
-        }
-
         final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
         if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
             noDreamAction.run();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4c49986..4c19322a 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1125,7 +1125,7 @@
         }
 
         mDisplayPolicy = new DisplayPolicy(mWmService, this);
-        mDisplayRotation = new DisplayRotation(mWmService, this);
+        mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address);
 
         mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
                 newFoldState -> {
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 185e06e..4fb137b 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -58,6 +58,7 @@
 import android.util.Slog;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
+import android.view.DisplayAddress;
 import android.view.IWindowManager;
 import android.view.Surface;
 import android.window.TransitionRequestInfo;
@@ -211,15 +212,16 @@
     private boolean mDemoHdmiRotationLock;
     private boolean mDemoRotationLock;
 
-    DisplayRotation(WindowManagerService service, DisplayContent displayContent) {
-        this(service, displayContent, displayContent.getDisplayPolicy(),
+    DisplayRotation(WindowManagerService service, DisplayContent displayContent,
+            DisplayAddress displayAddress) {
+        this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
                 service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock());
     }
 
     @VisibleForTesting
     DisplayRotation(WindowManagerService service, DisplayContent displayContent,
-            DisplayPolicy displayPolicy, DisplayWindowSettings displayWindowSettings,
-            Context context, Object lock) {
+            DisplayAddress displayAddress, DisplayPolicy displayPolicy,
+            DisplayWindowSettings displayWindowSettings, Context context, Object lock) {
         mService = service;
         mDisplayContent = displayContent;
         mDisplayPolicy = displayPolicy;
@@ -235,6 +237,8 @@
         mDeskDockRotation = readRotation(R.integer.config_deskDockRotation);
         mUndockedHdmiRotation = readRotation(R.integer.config_undockedHdmiRotation);
 
+        mRotation = readDefaultDisplayRotation(displayAddress);
+
         if (isDefaultDisplay) {
             final Handler uiHandler = UiThread.getHandler();
             mOrientationListener = new OrientationListener(mContext, uiHandler);
@@ -248,6 +252,33 @@
         }
     }
 
+    // Change the default value to the value specified in the sysprop
+    // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0,
+    // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270.
+    // If the value isn't specified or is ORIENTATION_0, nothing will be changed.
+    // This is needed to support having default orientation different from the natural
+    // device orientation. For example, on tablets that may want to keep natural orientation
+    // portrait for applications compatibility but have landscape orientation as a default choice
+    // from the UX perspective.
+    @Surface.Rotation
+    private int readDefaultDisplayRotation(DisplayAddress displayAddress) {
+        if (!(displayAddress instanceof DisplayAddress.Physical)) {
+            return Surface.ROTATION_0;
+        }
+        final DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) displayAddress;
+        String syspropValue = SystemProperties.get(
+                "ro.bootanim.set_orientation_" + physicalAddress.getPhysicalDisplayId(),
+                "ORIENTATION_0");
+        if (syspropValue.equals("ORIENTATION_90")) {
+            return Surface.ROTATION_90;
+        } else if (syspropValue.equals("ORIENTATION_180")) {
+            return Surface.ROTATION_180;
+        } else if (syspropValue.equals("ORIENTATION_270")) {
+            return Surface.ROTATION_270;
+        }
+        return Surface.ROTATION_0;
+    }
+
     private int readRotation(int resID) {
         try {
             final int rotation = mContext.getResources().getInteger(resID);
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 7bc8931..f628fba 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -74,6 +74,9 @@
                 <xs:element type="sensorDetails" name="lightSensor">
                     <xs:annotation name="final"/>
                 </xs:element>
+                <xs:element type="sensorDetails" name="screenOffBrightnessSensor">
+                    <xs:annotation name="final"/>
+                </xs:element>
                 <xs:element type="sensorDetails" name="proxSensor">
                     <xs:annotation name="final"/>
                 </xs:element>
@@ -109,6 +112,11 @@
                 <xs:element type="thresholds" name="ambientBrightnessChangeThresholdsIdle" minOccurs="0" maxOccurs="1">
                     <xs:annotation name="final"/>
                 </xs:element>
+                <!-- Table that translates sensor values from the screenOffBrightnessSensor
+                to lux values; -1 means the lux reading is not available. -->
+                <xs:element type="integer-array" name="screenOffBrightnessSensorValueToLux">
+                    <xs:annotation name="final"/>
+                </xs:element>
             </xs:sequence>
         </xs:complexType>
     </xs:element>
@@ -485,4 +493,10 @@
             </xs:element>
         </xs:sequence>
     </xs:complexType>
+
+    <xs:complexType name="integer-array">
+        <xs:sequence>
+            <xs:element name="item" type="xs:nonNegativeInteger" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
 </xs:schema>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 6276eda..cb08179 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -98,6 +98,8 @@
     method public final java.math.BigInteger getScreenBrightnessRampIncreaseMaxMillis();
     method public final java.math.BigDecimal getScreenBrightnessRampSlowDecrease();
     method public final java.math.BigDecimal getScreenBrightnessRampSlowIncrease();
+    method public final com.android.server.display.config.SensorDetails getScreenOffBrightnessSensor();
+    method public final com.android.server.display.config.IntegerArray getScreenOffBrightnessSensorValueToLux();
     method @NonNull public final com.android.server.display.config.ThermalThrottling getThermalThrottling();
     method public final void setAmbientBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds);
     method public final void setAmbientBrightnessChangeThresholdsIdle(com.android.server.display.config.Thresholds);
@@ -120,6 +122,8 @@
     method public final void setScreenBrightnessRampIncreaseMaxMillis(java.math.BigInteger);
     method public final void setScreenBrightnessRampSlowDecrease(java.math.BigDecimal);
     method public final void setScreenBrightnessRampSlowIncrease(java.math.BigDecimal);
+    method public final void setScreenOffBrightnessSensor(com.android.server.display.config.SensorDetails);
+    method public final void setScreenOffBrightnessSensorValueToLux(com.android.server.display.config.IntegerArray);
     method public final void setThermalThrottling(@NonNull com.android.server.display.config.ThermalThrottling);
   }
 
@@ -160,6 +164,11 @@
     method public final void setTransitionPoint_all(@NonNull java.math.BigDecimal);
   }
 
+  public class IntegerArray {
+    ctor public IntegerArray();
+    method public java.util.List<java.math.BigInteger> getItem();
+  }
+
   public class NitsMap {
     ctor public NitsMap();
     method public String getInterpolation();
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 6b705aa..86c5937 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -161,6 +161,14 @@
         assertArrayEquals(new int[]{70, 80},
                 mDisplayDeviceConfig.getHighAmbientBrightnessThresholds());
 
+        assertEquals("sensor_12345",
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor().type);
+        assertEquals("Sensor 12345",
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor().name);
+
+        assertArrayEquals(new int[]{-1, 10, 20, 30, 40},
+                mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux());
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -232,6 +240,7 @@
                 HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
         assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(),
                 HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -283,6 +292,10 @@
                 +       "<thermalStatusLimit>light</thermalStatusLimit>\n"
                 +       "<allowInLowPowerMode>false</allowInLowPowerMode>\n"
                 +   "</highBrightnessMode>\n"
+                +   "<screenOffBrightnessSensor>\n"
+                +       "<type>sensor_12345</type>\n"
+                +       "<name>Sensor 12345</name>\n"
+                +   "</screenOffBrightnessSensor>\n"
                 +   "<ambientBrightnessChangeThresholds>\n"
                 +       "<brighteningThresholds>\n"
                 +           "<minimum>10</minimum>\n"
@@ -467,6 +480,13 @@
                 +           "</blockingZoneThreshold>\n"
                 +       "</higherBlockingZoneConfigs>\n"
                 +   "</refreshRate>\n"
+                +   "<screenOffBrightnessSensorValueToLux>\n"
+                +       "<item>-1</item>\n"
+                +       "<item>10</item>\n"
+                +       "<item>20</item>\n"
+                +       "<item>30</item>\n"
+                +       "<item>40</item>\n"
+                +   "</screenOffBrightnessSensorValueToLux>\n"
                 + "</displayConfiguration>\n";
     }
 
@@ -544,6 +564,7 @@
         when(mResources.getIntArray(
                 R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
                 .thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+
         mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
new file mode 100644
index 0000000..ea04a19
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 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.server.display;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ScreenOffBrightnessSensorControllerTest {
+
+    private static final int[] SENSOR_TO_LUX = new int[]{-1, 10, 20, 30, 40};
+
+    private ScreenOffBrightnessSensorController mController;
+    private OffsettableClock mClock;
+    private Sensor mLightSensor;
+
+    @Mock SensorManager mSensorManager;
+    @Mock Handler mNoOpHandler;
+    @Mock BrightnessMappingStrategy mBrightnessMappingStrategy;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mClock = new OffsettableClock.Stopped();
+        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
+        mController = new ScreenOffBrightnessSensorController(
+                mSensorManager,
+                mLightSensor,
+                mNoOpHandler,
+                mClock::now,
+                SENSOR_TO_LUX,
+                mBrightnessMappingStrategy
+        );
+    }
+
+    @Test
+    public void testBrightness() throws Exception {
+        when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor),
+                eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class)))
+                .thenReturn(true);
+        mController.setLightSensorEnabled(true);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                mController.getAutomaticScreenBrightness(), 0);
+
+        int sensorValue = 1;
+        float brightness = 0.2f;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+        when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+                .thenReturn(brightness);
+        assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+        sensorValue = 2;
+        brightness = 0.4f;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+        when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+                .thenReturn(brightness);
+        assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+        sensorValue = 3;
+        brightness = 0.6f;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+        when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+                .thenReturn(brightness);
+        assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+        sensorValue = 4;
+        brightness = 0.8f;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+        when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+                .thenReturn(brightness);
+        assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+        sensorValue = 5;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+        assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                mController.getAutomaticScreenBrightness(), 0);
+    }
+
+    @Test
+    public void testSensorValueValidTime() throws Exception {
+        when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor),
+                eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class)))
+                .thenReturn(true);
+        mController.setLightSensorEnabled(true);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1));
+        mController.setLightSensorEnabled(false);
+        assertNotEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                mController.getAutomaticScreenBrightness(), 0);
+
+        mClock.fastForward(2000);
+        mController.setLightSensorEnabled(false);
+        assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                mController.getAutomaticScreenBrightness(), 0);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/TestUtils.java b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
index 0454587..f3f04b8 100644
--- a/services/tests/servicestests/src/com/android/server/display/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
@@ -28,13 +28,13 @@
 
 public final class TestUtils {
 
-    public static SensorEvent createSensorEvent(Sensor sensor, int lux) throws Exception {
+    public static SensorEvent createSensorEvent(Sensor sensor, int value) throws Exception {
         final Constructor<SensorEvent> constructor =
                 SensorEvent.class.getDeclaredConstructor(int.class);
         constructor.setAccessible(true);
         final SensorEvent event = constructor.newInstance(1);
         event.sensor = sensor;
-        event.values[0] = lux;
+        event.values[0] = value;
         event.timestamp = SystemClock.elapsedRealtimeNanos();
         return event;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index b45c37f..491f876d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -60,6 +60,7 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
+import android.view.DisplayAddress;
 import android.view.Surface;
 import android.view.WindowManager;
 
@@ -101,6 +102,7 @@
     private static WindowManagerService sMockWm;
     private DisplayContent mMockDisplayContent;
     private DisplayPolicy mMockDisplayPolicy;
+    private DisplayAddress mMockDisplayAddress;
     private Context mMockContext;
     private Resources mMockRes;
     private SensorManager mMockSensorManager;
@@ -1091,9 +1093,11 @@
             when(mMockResolver.acquireProvider(Settings.AUTHORITY))
                     .thenReturn(mFakeSettingsProvider.getIContentProvider());
 
+            mMockDisplayAddress = mock(DisplayAddress.class);
+
             mMockDisplayWindowSettings = mock(DisplayWindowSettings.class);
-            mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayPolicy,
-                    mMockDisplayWindowSettings, mMockContext, new Object());
+            mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
+                    mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object());
             reset(sMockWm);
 
             captureObservers();