Merge "Remove unnecessary view ID" 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/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 68e1acb..132bd66 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -608,7 +608,10 @@
             WAKE_REASON_DISPLAY_GROUP_ADDED,
             WAKE_REASON_DISPLAY_GROUP_TURNED_ON,
             WAKE_REASON_UNFOLD_DEVICE,
-            WAKE_REASON_DREAM_FINISHED
+            WAKE_REASON_DREAM_FINISHED,
+            WAKE_REASON_TAP,
+            WAKE_REASON_LIFT,
+            WAKE_REASON_BIOMETRIC,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface WakeReason{}
@@ -660,8 +663,9 @@
     public static final int WAKE_REASON_PLUGGED_IN = 3;
 
     /**
-     * Wake up reason code: Waking up due to a user performed gesture (e.g. double tapping on the
-     * screen).
+     * Wake up reason code: Waking up due to a user performed gesture. This includes user
+     * interactions with UI on the screen such as the notification shade. This does not include
+     * {@link WAKE_REASON_TAP} or {@link WAKE_REASON_LIFT}.
      * @hide
      */
     public static final int WAKE_REASON_GESTURE = 4;
@@ -723,6 +727,26 @@
     public static final int WAKE_REASON_DREAM_FINISHED = 13;
 
     /**
+     * Wake up reason code: Waking up due to the user single or double tapping on the screen. This
+     * wake reason is used when the user is not tapping on a specific UI element; rather, the device
+     * wakes up due to a generic tap on the screen.
+     * @hide
+     */
+    public static final int WAKE_REASON_TAP = 15;
+
+    /**
+     * Wake up reason code: Waking up due to a user performed lift gesture.
+     * @hide
+     */
+    public static final int WAKE_REASON_LIFT = 16;
+
+    /**
+     * Wake up reason code: Waking up due to a user interacting with a biometric.
+     * @hide
+     */
+    public static final int WAKE_REASON_BIOMETRIC = 17;
+
+    /**
      * Convert the wake reason to a string for debugging purposes.
      * @hide
      */
@@ -742,6 +766,9 @@
             case WAKE_REASON_DISPLAY_GROUP_TURNED_ON: return "WAKE_REASON_DISPLAY_GROUP_TURNED_ON";
             case WAKE_REASON_UNFOLD_DEVICE: return "WAKE_REASON_UNFOLD_DEVICE";
             case WAKE_REASON_DREAM_FINISHED: return "WAKE_REASON_DREAM_FINISHED";
+            case WAKE_REASON_TAP: return "WAKE_REASON_TAP";
+            case WAKE_REASON_LIFT: return "WAKE_REASON_LIFT";
+            case WAKE_REASON_BIOMETRIC: return "WAKE_REASON_BIOMETRIC";
             default: return Integer.toString(wakeReason);
         }
     }
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 950c8ac..ed24740 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -58,19 +58,24 @@
      * Manifest metadata to show a custom embedded activity as part of device controls.
      *
      * The value of this metadata must be the {@link ComponentName} as a string of an activity in
-     * the same package that will be launched as part of a TaskView.
+     * the same package that will be launched embedded in the device controls space.
      *
      * The activity must be exported, enabled and protected by
      * {@link Manifest.permission.BIND_CONTROLS}.
      *
+     * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
+     *
      * @hide
      */
     public static final String META_DATA_PANEL_ACTIVITY =
             "android.service.controls.META_DATA_PANEL_ACTIVITY";
 
     /**
-     * Boolean extra containing the value of
-     * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+     * Boolean extra containing the value of the setting allowing actions on a locked device.
+     *
+     * This corresponds to the setting that indicates whether the user has
+     * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as
+     * {@code false} when the device is locked.
      *
      * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
      * is launched.
@@ -78,7 +83,7 @@
      * @hide
      */
     public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
-            "android.service.controls.extra.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+            "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
 
     /**
      * @hide
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/config.xml b/core/res/res/values/config.xml
index 22f4298..50761bf 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5204,6 +5204,15 @@
          the format [System DeviceState]:[WM Jetpack Posture], for example: "0:1". -->
     <string-array name="config_device_state_postures" translatable="false" />
 
+    <!-- Which Surface rotations are considered as tabletop posture (horizontal hinge) when the
+         device is half-folded. Other half-folded postures will be assumed to be book (vertical
+         hinge) mode. Units: degrees; valid values: 0, 90, 180, 270. -->
+    <integer-array name="config_deviceTabletopRotations" />
+
+    <!-- This flag indicates that a display with fold-state FLAT should always be considered as
+         having a separating hinge. -->
+    <bool name="config_isDisplayHingeAlwaysSeparating">false</bool>
+
     <!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored.
          Note: Activity min/max aspect ratio restrictions will still be respected.
          Therefore this override can control the maximum screen area that can be occupied by
@@ -5252,14 +5261,26 @@
 
     <!-- Horizontal position of a center of the letterboxed app window.
         0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
-        or > 1, it is ignored and central position is used (0.5). -->
+        or > 1 it is ignored and for non-book mode central position is used (0.5); for book mode
+         left is used (0.0). -->
     <item name="config_letterboxHorizontalPositionMultiplier" format="float" type="dimen">0.5</item>
 
     <!-- Vertical position of a center of the letterboxed app window.
         0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
-        or > 1, it is ignored and central position is used (0.5). -->
+        or > 1 it is ignored and for non-tabletop mode central position is used (0.5); for
+         tabletop mode top (0.0) is used. -->
     <item name="config_letterboxVerticalPositionMultiplier" format="float" type="dimen">0.0</item>
 
+    <!-- Horizontal position of a center of the letterboxed app window when in book mode.
+    0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
+    or > 1, it is ignored and left position is used (0.0). -->
+    <item name="config_letterboxBookModePositionMultiplier" format="float" type="dimen">0.0</item>
+
+    <!-- Vertical position of a center of the letterboxed app window when in tabletop mode.
+        0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
+        or > 1, it is ignored and top position is used (0.0). -->
+    <item name="config_letterboxTabletopModePositionMultiplier" format="float" type="dimen">0.0</item>
+
     <!-- Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps.
     -->
     <bool name="config_letterboxIsHorizontalReachabilityEnabled">false</bool>
@@ -5287,6 +5308,26 @@
         If given value is outside of this range, the option 1 (center) is assummed. -->
     <integer name="config_letterboxDefaultPositionForVerticalReachability">1</integer>
 
+    <!-- Default horizontal position of the letterboxed app window when reachability is
+    enabled and an app is fullscreen in landscape device orientation and in book mode. When
+    reachability is enabled, the position can change between left, center and right. This config
+    defines the default one:
+        - Option 0 - Left.
+        - Option 1 - Center.
+        - Option 2 - Right.
+    If given value is outside of this range, the option 0 (left) is assummed. -->
+    <integer name="config_letterboxDefaultPositionForBookModeReachability">0</integer>
+
+    <!-- Default vertical position of the letterboxed app window when reachability is
+        enabled and an app is fullscreen in portrait device orientation and in tabletop mode. When
+        reachability is enabled, the position can change between top, center and bottom. This config
+        defines the default one:
+            - Option 0 - Top.
+            - Option 1 - Center.
+            - Option 2 - Bottom.
+        If given value is outside of this range, the option 0 (top) is assummed. -->
+    <integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer>
+
     <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
     <bool name="config_letterboxIsEducationEnabled">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ba274a7..fb77b3b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4435,6 +4435,8 @@
   <java-symbol type="array" name="config_keep_warming_services" />
   <java-symbol type="string" name="config_display_features" />
   <java-symbol type="array" name="config_device_state_postures" />
+  <java-symbol type="array" name="config_deviceTabletopRotations" />
+  <java-symbol type="bool" name="config_isDisplayHingeAlwaysSeparating" />
 
   <java-symbol type="dimen" name="controls_thumbnail_image_max_height" />
   <java-symbol type="dimen" name="controls_thumbnail_image_max_width" />
@@ -4447,10 +4449,14 @@
   <java-symbol type="color" name="config_letterboxBackgroundColor" />
   <java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" />
   <java-symbol type="dimen" name="config_letterboxVerticalPositionMultiplier" />
+  <java-symbol type="dimen" name="config_letterboxBookModePositionMultiplier" />
+  <java-symbol type="dimen" name="config_letterboxTabletopModePositionMultiplier" />
   <java-symbol type="bool" name="config_letterboxIsHorizontalReachabilityEnabled" />
   <java-symbol type="bool" name="config_letterboxIsVerticalReachabilityEnabled" />
   <java-symbol type="integer" name="config_letterboxDefaultPositionForHorizontalReachability" />
   <java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" />
+  <java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" />
+  <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
   <java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 86c8097..49704d9 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1525,6 +1525,12 @@
       "group": "WM_DEBUG_FOCUS_LIGHT",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-637815408": {
+      "message": "Invalid surface rotation angle in config_deviceTabletopRotations: %d",
+      "level": "ERROR",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotation.java"
+    },
     "-636553602": {
       "message": "commitVisibility: %s: visible=%b visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s",
       "level": "VERBOSE",
@@ -3193,6 +3199,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "939638078": {
+      "message": "config_deviceTabletopRotations is not defined. Half-fold letterboxing will work inconsistently.",
+      "level": "WARN",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotation.java"
+    },
     "948208142": {
       "message": "Setting Activity.mLauncherTaskBehind to true. Activity=%s",
       "level": "DEBUG",
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/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index f55fb97..9058510 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -169,11 +169,9 @@
             setFloatUniform("in_progress", value)
             val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
 
-            setFloatUniform(
-                "in_size",
-                /* width= */ maxSize.x * curvedProg,
-                /* height= */ maxSize.y * curvedProg
-            )
+            currentWidth = maxSize.x * curvedProg
+            currentHeight = maxSize.y * curvedProg
+            setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
             setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
             // radius should not exceed width and height values.
             setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@
      * False for a ring effect.
      */
     var rippleFill: Boolean = false
+
+    var currentWidth: Float = 0f
+        private set
+
+    var currentHeight: Float = 0f
+        private set
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index ae28a8b..b37c734 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -36,7 +36,7 @@
  */
 open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
 
-    private lateinit var rippleShader: RippleShader
+    protected lateinit var rippleShader: RippleShader
     lateinit var rippleShape: RippleShape
         private set
 
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/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 87d2c51..f37d221 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1272,6 +1272,9 @@
          translate into their final position. -->
     <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
 
+    <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+    <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
     <!-- The amount of vertical offset for the keyguard during the full shade transition. -->
     <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
 
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/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 8b9823b..b8e196f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static java.util.Collections.emptySet;
+
 import android.content.Context;
 import android.os.Trace;
 import android.util.AttributeSet;
@@ -88,8 +90,9 @@
     }
 
     /** Sets a translationY value on every child view except for the media view. */
-    public void setChildrenTranslationYExcludingMediaView(float translationY) {
-        setChildrenTranslationYExcluding(translationY, Set.of(mMediaHostContainer));
+    public void setChildrenTranslationY(float translationY, boolean excludeMedia) {
+        setChildrenTranslationYExcluding(translationY,
+                excludeMedia ? Set.of(mMediaHostContainer) : emptySet());
     }
 
     /** Sets a translationY value on every view except for the views in the provided set. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7849747..aec3063 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,8 @@
 import android.util.Slog;
 
 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -59,6 +61,7 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             ConfigurationController configurationController,
             DozeParameters dozeParameters,
+            FeatureFlags featureFlags,
             ScreenOffAnimationController screenOffAnimationController) {
         super(keyguardStatusView);
         mKeyguardSliceViewController = keyguardSliceViewController;
@@ -67,6 +70,8 @@
         mConfigurationController = configurationController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
                 dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+        mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
+                featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
     }
 
     @Override
@@ -115,8 +120,8 @@
     /**
      * Sets a translationY on the views on the keyguard, except on the media view.
      */
-    public void setTranslationYExcludingMedia(float translationY) {
-        mView.setChildrenTranslationYExcludingMediaView(translationY);
+    public void setTranslationY(float translationY, boolean excludeMedia) {
+        mView.setChildrenTranslationY(translationY, excludeMedia);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 498304b..bde0692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -44,6 +44,7 @@
     private boolean mAnimateYPos;
     private boolean mKeyguardViewVisibilityAnimating;
     private boolean mLastOccludedState = false;
+    private boolean mIsUnoccludeTransitionFlagEnabled = false;
     private final AnimationProperties mAnimationProperties = new AnimationProperties();
 
     public KeyguardVisibilityHelper(View view,
@@ -62,6 +63,10 @@
         return mKeyguardViewVisibilityAnimating;
     }
 
+    public void setOcclusionTransitionFlagEnabled(boolean enabled) {
+        mIsUnoccludeTransitionFlagEnabled = enabled;
+    }
+
     /**
      * Set the visibility of a keyguard view based on some new state.
      */
@@ -129,7 +134,7 @@
                 // since it may need to be cancelled due to keyguard lifecycle events.
                 mScreenOffAnimationController.animateInKeyguard(
                         mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
-            } else if (mLastOccludedState && !isOccluded) {
+            } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
                 // An activity was displayed over the lock screen, and has now gone away
                 mView.setVisibility(View.VISIBLE);
                 mView.setAlpha(0f);
@@ -142,7 +147,9 @@
                         .start();
             } else {
                 mView.setVisibility(View.VISIBLE);
-                mView.setAlpha(1f);
+                if (!mIsUnoccludeTransitionFlagEnabled) {
+                    mView.setAlpha(1f);
+                }
             }
         } else {
             mView.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 833ff3f..37183e8 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
 
 import android.annotation.AnyThread;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
@@ -40,6 +41,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.R;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
@@ -140,6 +142,7 @@
     }
 
     DozeSensors(
+            Resources resources,
             AsyncSensorManager sensorManager,
             DozeParameters dozeParameters,
             AmbientDisplayConfiguration config,
@@ -185,7 +188,8 @@
                 new TriggerSensor(
                         mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
                         Settings.Secure.DOZE_PICK_UP_GESTURE,
-                        true /* settingDef */,
+                        resources.getBoolean(
+                                R.bool.config_dozePickupGestureEnabled) /* settingDef */,
                         config.dozePickupSensorAvailable(),
                         DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */,
                         false /* touchscreen */,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 3f9f14c..b95c3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -210,7 +210,7 @@
         mAllowPulseTriggers = true;
         mSessionTracker = sessionTracker;
 
-        mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
+        mDozeSensors = new DozeSensors(mContext.getResources(), mSensorManager, dozeParameters,
                 config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
                 secureSettings, authController, devicePostureController, userTracker);
         mDockManager = dockManager;
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/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 339fd13..1f80b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -181,10 +181,6 @@
     @JvmField
     val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
 
-    // TODO(b/262780002): Tracking Bug
-    @JvmField
-    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
     /** Flag to control the migration of face auth to modern architecture. */
     // TODO(b/262838215): Tracking bug
     @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -193,6 +189,15 @@
     // TODO(b/244313043): Tracking bug
     @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
 
+    // TODO(b/262780002): Tracking Bug
+    @JvmField
+    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
+    /** A different path for unocclusion transitions back to keyguard */
+    // TODO(b/262859270): Tracking Bug
+    @JvmField
+    val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -310,6 +315,10 @@
     // TODO(b/261734857): Tracking Bug
     @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
 
+    // TODO(b/263272731): Tracking Bug
+    val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
+        unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d231870..8aada1f 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.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_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) LOCKSCREEN_ANIMATION_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..188930c 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,15 @@
         }
     }
 
-    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
     }
 }
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..402fac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.animation.Interpolators.EMPHASIZED_DECELERATE
+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 }
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * 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
+        @JvmField
+        val LOCKSCREEN_ANIMATION_DURATION_MS =
+            (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
+
+        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
+        val DREAM_OVERLAY_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/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 03bc935..8a565fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -26,4 +26,8 @@
 class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
     /** */
     fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
+
+    /** Check whether the flag for the receiver success state is enabled. */
+    fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
+        featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
 }
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..889147b 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,
@@ -111,6 +114,9 @@
         }
     }
 
+    private var maxRippleWidth: Float = 0f
+    private var maxRippleHeight: Float = 0f
+
     private fun updateMediaTapToTransferReceiverDisplay(
         @StatusBarManager.MediaTransferReceiverState displayState: Int,
         routeInfo: MediaRoute2Info,
@@ -162,6 +168,7 @@
     }
 
     override fun start() {
+        super.start()
         if (mediaTttFlags.isMediaTttEnabled()) {
             commandQueue.addCallback(commandQueueCallbacks)
         }
@@ -212,7 +219,7 @@
         expandRipple(view.requireViewById(R.id.ripple))
     }
 
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         val appIconView = view.getAppIconView()
         appIconView.animate()
                 .translationYBy(getTranslationAmount().toFloat())
@@ -222,7 +229,14 @@
                 .alpha(0f)
                 .setDuration(ICON_ALPHA_ANIM_DURATION)
                 .start()
-        (view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
+
+        val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+        if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
+                mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
+            expandRippleToFull(rippleView, onAnimationEnd)
+        } else {
+            rippleView.collapseRipple(onAnimationEnd)
+        }
     }
 
     override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -267,12 +281,19 @@
         })
     }
 
-    private fun layoutRipple(rippleView: ReceiverChipRippleView) {
+    private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
         val windowBounds = windowManager.currentWindowMetrics.bounds
         val height = windowBounds.height().toFloat()
         val width = windowBounds.width().toFloat()
 
-        rippleView.setMaxSize(width / 2f, height / 2f)
+        if (isFullScreen) {
+            maxRippleHeight = height * 2f
+            maxRippleWidth = width * 2f
+        } else {
+            maxRippleHeight = height / 2f
+            maxRippleWidth = width / 2f
+        }
+        rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
         // Center the ripple on the bottom of the screen in the middle.
         rippleView.setCenter(width * 0.5f, height)
         val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -282,6 +303,11 @@
     private fun View.getAppIconView(): CachingIconView {
         return this.requireViewById(R.id.app_icon)
     }
+
+    private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+        layoutRipple(rippleView, true)
+        rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+    }
 }
 
 val ICON_TRANSLATION_ANIM_DURATION = 30.frames
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 6e9fc5c..87b2528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -22,6 +22,7 @@
 import android.util.AttributeSet
 import com.android.systemui.surfaceeffects.ripple.RippleShader
 import com.android.systemui.surfaceeffects.ripple.RippleView
+import kotlin.math.pow
 
 /**
  * An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@
         })
         animator.reverse()
     }
+
+    // Expands the ripple to cover full screen.
+    fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
+        if (!isStarted) {
+            return
+        }
+        // Reset all listeners to animator.
+        animator.removeAllListeners()
+        animator.removeAllUpdateListeners()
+
+        // Only show the outline as ripple expands and disappears when animation ends.
+        setRippleFill(false)
+
+        val startingPercentage = calculateStartingPercentage(newHeight)
+        animator.addUpdateListener { updateListener ->
+            val now = updateListener.currentPlayTime
+            val progress = updateListener.animatedValue as Float
+            rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
+            rippleShader.distortionStrength = 1 - rippleShader.progress
+            rippleShader.pixelDensity = 1 - rippleShader.progress
+            rippleShader.time = now.toFloat()
+            invalidate()
+        }
+        animator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator?) {
+                animation?.let { visibility = GONE }
+                onAnimationEnd?.run()
+                isStarted = false
+            }
+        })
+        animator.start()
+    }
+
+    // Calculates the actual starting percentage according to ripple shader progress set method.
+    // Check calculations in [RippleShader.progress]
+    fun calculateStartingPercentage(newHeight: Float): Float {
+        val ratio = rippleShader.currentHeight / newHeight
+        val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
+        return 1 - remainingPercentage
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index fcdde79..8054f27 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -44,6 +44,7 @@
 import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import static java.lang.Float.isNaN;
 
@@ -138,6 +139,10 @@
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -677,6 +682,12 @@
     private boolean mGestureWaitForTouchSlop;
     private boolean mIgnoreXTouchSlop;
     private boolean mExpandLatencyTracking;
+    private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private boolean mIsDreamToLockscreenTransitionRunning = false;
+    private int mDreamingToLockscreenTransitionTranslationY;
+    private boolean mUnocclusionTransitionFlagEnabled = false;
 
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -692,6 +703,12 @@
         }
     };
 
+    private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
+            (TransitionStep step) -> {
+                mIsDreamToLockscreenTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
             @Main Handler handler,
@@ -760,6 +777,8 @@
             SystemClock systemClock,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+            DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
             DumpManager dumpManager) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
@@ -775,6 +794,8 @@
         mShadeLog = shadeLogger;
         mShadeHeightLogger = shadeHeightLogger;
         mGutsManager = gutsManager;
+        mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -920,6 +941,8 @@
         mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
                 SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
 
+        mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+
         mQsFrameTranslateController = qsFrameTranslateController;
         updateUserSwitcherFlags();
         mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1072,6 +1095,18 @@
         mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
+
+        if (mUnocclusionTransitionFlagEnabled) {
+            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+                    dreamingToLockscreenTransitionAlpha(mNotificationStackScrollLayoutController));
+
+            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+                    mDreamingToLockscreenTransitionTranslationY),
+                    dreamingToLockscreenTransitionY(mNotificationStackScrollLayoutController));
+
+            collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+                    mDreamingToLockscreenTransition);
+        }
     }
 
     @VisibleForTesting
@@ -1106,6 +1141,8 @@
         mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
         mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
                 R.dimen.split_shade_scrim_transition_distance);
+        mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
     }
 
     private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1769,10 +1806,14 @@
     }
 
     private void updateClock() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
         mKeyguardStatusViewController.setAlpha(alpha);
         mKeyguardStatusViewController
-                .setTranslationYExcludingMedia(mKeyguardOnlyTransitionTranslationY);
+            .setTranslationY(mKeyguardOnlyTransitionTranslationY, /* excludeMedia= */true);
+
         if (mKeyguardQsUserSwitchController != null) {
             mKeyguardQsUserSwitchController.setAlpha(alpha);
         }
@@ -2656,7 +2697,9 @@
         } else if (statusBarState == KEYGUARD
                 || statusBarState == StatusBarState.SHADE_LOCKED) {
             mKeyguardBottomArea.setVisibility(View.VISIBLE);
-            mKeyguardBottomArea.setAlpha(1f);
+            if (!mIsDreamToLockscreenTransitionRunning) {
+                mKeyguardBottomArea.setAlpha(1f);
+            }
         } else {
             mKeyguardBottomArea.setVisibility(View.GONE);
         }
@@ -3523,6 +3566,9 @@
     }
 
     private void updateNotificationTranslucency() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         float alpha = 1f;
         if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
                 && !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -3578,6 +3624,9 @@
     }
 
     private void updateKeyguardBottomAreaAlpha() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         // There are two possible panel expansion behaviors:
         // • User dragging up to unlock: we want to fade out as quick as possible
         //   (ALPHA_EXPANSION_THRESHOLD) to avoid seeing the bouncer over the bottom area.
@@ -3610,7 +3659,9 @@
     }
 
     private void onExpandingFinished() {
-        mScrimController.onExpandingFinished();
+        if (!mUnocclusionTransitionFlagEnabled) {
+            mScrimController.onExpandingFinished();
+        }
         mNotificationStackScrollLayoutController.onExpansionStopped();
         mHeadsUpManager.onExpandingFinished();
         mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -5805,6 +5856,32 @@
         mCurrentPanelState = state;
     }
 
+    private Consumer<Float> dreamingToLockscreenTransitionAlpha(
+            NotificationStackScrollLayoutController stackScroller) {
+        return (Float alpha) -> {
+            mKeyguardStatusViewController.setAlpha(alpha);
+            stackScroller.setAlpha(alpha);
+
+            mKeyguardBottomAreaInteractor.setAlpha(alpha);
+            mLockIconViewController.setAlpha(alpha);
+
+            if (mKeyguardQsUserSwitchController != null) {
+                mKeyguardQsUserSwitchController.setAlpha(alpha);
+            }
+            if (mKeyguardUserSwitcherController != null) {
+                mKeyguardUserSwitcherController.setAlpha(alpha);
+            }
+        };
+    }
+
+    private Consumer<Float> dreamingToLockscreenTransitionY(
+                NotificationStackScrollLayoutController stackScroller) {
+        return (Float translationY) -> {
+            mKeyguardStatusViewController.setTranslationY(translationY,  /* excludeMedia= */false);
+            stackScroller.setTranslationY(translationY);
+        };
+    }
+
     @VisibleForTesting
     StatusBarStateController getStatusBarStateController() {
         return mStatusBarStateController;
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/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 2334a4c..9421524 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -90,8 +90,13 @@
 class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
 
     // Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster
-    private val interpolator = PathInterpolator(/* controlX1= */ 0.4f, /* controlY1= */ 0f,
-            /* controlX2= */ 0.2f, /* controlY2= */ 1f)
+    private val interpolator =
+        PathInterpolator(
+            /* controlX1= */ 0.4f,
+            /* controlY1= */ 0f,
+            /* controlX2= */ 0.2f,
+            /* controlY2= */ 1f
+        )
 
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         val interpolatedAmount = interpolator.getInterpolation(amount)
@@ -116,17 +121,17 @@
 
         if (isVertical) {
             scrim.setRevealGradientBounds(
-                left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount,
+                left = scrim.viewWidth / 2 - (scrim.viewWidth / 2) * gradientBoundsAmount,
                 top = 0f,
-                right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount,
-                bottom = scrim.height.toFloat()
+                right = scrim.viewWidth / 2 + (scrim.viewWidth / 2) * gradientBoundsAmount,
+                bottom = scrim.viewHeight.toFloat()
             )
         } else {
             scrim.setRevealGradientBounds(
                 left = 0f,
-                top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount,
-                right = scrim.width.toFloat(),
-                bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount
+                top = scrim.viewHeight / 2 - (scrim.viewHeight / 2) * gradientBoundsAmount,
+                right = scrim.viewWidth.toFloat(),
+                bottom = scrim.viewHeight / 2 + (scrim.viewHeight / 2) * gradientBoundsAmount
             )
         }
     }
@@ -234,7 +239,14 @@
  * transparent center. The center position, size, and stops of the gradient can be manipulated to
  * reveal views below the scrim as if they are being 'lit up'.
  */
-class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+class LightRevealScrim
+@JvmOverloads
+constructor(
+    context: Context?,
+    attrs: AttributeSet?,
+    initialWidth: Int? = null,
+    initialHeight: Int? = null
+) : View(context, attrs) {
 
     /** Listener that is called if the scrim's opaqueness changes */
     lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
@@ -278,6 +290,17 @@
     var revealGradientHeight: Float = 0f
 
     /**
+     * Keeps the initial value until the view is measured. See [LightRevealScrim.onMeasure].
+     *
+     * Needed as the view dimensions are used before the onMeasure pass happens, and without preset
+     * width and height some flicker during fold/unfold happens.
+     */
+    internal var viewWidth: Int = initialWidth ?: 0
+        private set
+    internal var viewHeight: Int = initialHeight ?: 0
+        private set
+
+    /**
      * Alpha of the fill that can be used in the beginning of the animation to hide the content.
      * Normally the gradient bounds are animated from small size so the content is not visible, but
      * if the start gradient bounds allow to see some content this could be used to make the reveal
@@ -375,6 +398,11 @@
         invalidate()
     }
 
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        viewWidth = measuredWidth
+        viewHeight = measuredHeight
+    }
     /**
      * Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is
      * simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4bcc0b6..c2c38a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -916,6 +916,11 @@
         return mView.getTranslationX();
     }
 
+    /** Set view y-translation */
+    public void setTranslationY(float translationY) {
+        mView.setTranslationY(translationY);
+    }
+
     public int indexOfChild(View view) {
         return mView.indexOfChild(view);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d500f99..ee8b861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -232,7 +232,6 @@
     private boolean mExpansionAffectsAlpha = true;
     private boolean mAnimateChange;
     private boolean mUpdatePending;
-    private boolean mTracking;
     private long mAnimationDuration = -1;
     private long mAnimationDelay;
     private Animator.AnimatorListener mAnimatorListener;
@@ -526,7 +525,6 @@
     }
 
     public void onTrackingStarted() {
-        mTracking = true;
         mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
         if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
             mAnimatingPanelExpansionOnUnlock = false;
@@ -534,7 +532,6 @@
     }
 
     public void onExpandingFinished() {
-        mTracking = false;
         setUnocclusionAnimationRunning(false);
     }
 
@@ -1450,8 +1447,6 @@
         pw.print(" expansionProgress=");
         pw.println(mTransitionToLockScreenFullShadeNotificationsProgress);
 
-        pw.print("  mTracking=");
-        pw.println(mTracking);
         pw.print("  mDefaultScrimAlpha=");
         pw.println(mDefaultScrimAlpha);
         pw.print("  mPanelExpansionFraction=");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f196505..d480fab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -135,7 +135,7 @@
     private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final BouncerView mPrimaryBouncerView;
-    private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
+    private final Lazy<ShadeController> mShadeController;
 
     // Local cache of expansion events, to avoid duplicates
     private float mFraction = -1f;
@@ -252,6 +252,7 @@
     private float mQsExpansion;
     final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
     private boolean mIsModernBouncerEnabled;
+    private boolean mIsUnoccludeTransitionFlagEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
     private Runnable mKeyguardGoneCancelAction;
@@ -329,6 +330,7 @@
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
+        mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
     }
 
     @Override
@@ -867,8 +869,10 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
-            mCentralSurfaces.animateKeyguardUnoccluding();
+        if (!mIsUnoccludeTransitionFlagEnabled) {
+            if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
+                mCentralSurfaces.animateKeyguardUnoccluding();
+            }
         }
     }
 
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..6c37f94 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
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
 import android.annotation.IntRange
-import android.telephony.Annotation.DataActivityType
 import android.telephony.CellSignalStrength
 import android.telephony.TelephonyCallback.CarrierNetworkListener
 import android.telephony.TelephonyCallback.DataActivityListener
@@ -28,6 +27,7 @@
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 
 /**
  * Data class containing all of the relevant information for a particular line of service, known as
@@ -39,28 +39,42 @@
  * threading complex system objects through the pipeline.
  */
 data class MobileConnectionModel(
-    /** From [ServiceStateListener.onServiceStateChanged] */
+    /** Fields below are from [ServiceStateListener.onServiceStateChanged] */
     val isEmergencyOnly: Boolean = false,
+    val isRoaming: Boolean = false,
+    /**
+     * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the
+     * current registered operator name in short alphanumeric format. In some cases this name might
+     * be preferred over other methods of calculating the network name
+     */
+    val operatorAlphaShort: String? = null,
 
-    /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+    /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */
     val isGsm: Boolean = false,
     @IntRange(from = 0, to = 4)
     val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
     @IntRange(from = 0, to = 4)
     val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
 
-    /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */
+    /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */
     val dataConnectionState: DataConnectionState = Disconnected,
 
-    /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
-    @DataActivityType val dataActivityDirection: Int? = null,
+    /**
+     * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the
+     * values
+     */
+    val dataActivityDirection: DataActivityModel =
+        DataActivityModel(
+            hasActivityIn = false,
+            hasActivityOut = false,
+        ),
 
-    /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+    /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */
     val carrierNetworkChangeActive: Boolean = false,
 
+    /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */
+
     /**
-     * From [DisplayInfoListener.onDisplayInfoChanged].
-     *
      * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
      * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
new file mode 100644
index 0000000..a8cf35a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.model
+
+import android.content.Intent
+import android.telephony.TelephonyManager.EXTRA_DATA_SPN
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+
+/**
+ * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from
+ * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED].
+ */
+sealed interface NetworkNameModel {
+    val name: String
+
+    /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */
+    data class Default(override val name: String) : NetworkNameModel
+
+    /**
+     * This name has been derived from telephony intents. see
+     * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]
+     */
+    data class Derived(override val name: String) : NetworkNameModel
+}
+
+fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? {
+    val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false)
+    val spn = getStringExtra(EXTRA_DATA_SPN)
+    val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false)
+    val plmn = getStringExtra(EXTRA_PLMN)
+
+    val str = StringBuilder()
+    val strData = StringBuilder()
+    if (showPlmn && plmn != null) {
+        str.append(plmn)
+        strData.append(plmn)
+    }
+    if (showSpn && spn != null) {
+        if (str.isNotEmpty()) {
+            str.append(separator)
+        }
+        str.append(spn)
+    }
+
+    return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null
+}
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..2fd415e6 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
@@ -21,6 +21,7 @@
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -50,4 +51,15 @@
      * [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>
+
+    /** The service provider name for this network connection, or the default name */
+    val networkName: StateFlow<NetworkNameModel>
 }
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..d3ee85f 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
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
 import android.util.Log
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.MobileMappings
@@ -26,6 +27,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -34,6 +36,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -185,7 +188,9 @@
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
         connection.isDefaultDataSubscription.value = state.dataType != null
+        connection.networkName.value = NetworkNameModel.Derived(state.name)
 
+        connection.cdmaRoaming.value = state.roaming
         connection.connectionInfo.value = state.toMobileConnectionModel()
     }
 
@@ -229,12 +234,13 @@
     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,
             dataConnectionState =
                 DataConnectionState.Connected, // TODO(b/261029387): not yet supported
-            dataActivityDirection = activity,
+            dataActivityDirection = (activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(),
             carrierNetworkChangeActive = carrierNetworkChange,
             resolvedNetworkType = dataType.toResolvedNetworkType()
         )
@@ -260,4 +266,8 @@
     override val dataEnabled = MutableStateFlow(true)
 
     override val isDefaultDataSubscription = MutableStateFlow(true)
+
+    override val cdmaRoaming = MutableStateFlow(false)
+
+    override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network"))
 }
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..a1ae8ed 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,8 @@
         val inflateStrength = getString("inflate")?.toBoolean()
         val activity = getString("activity")?.toActivity()
         val carrierNetworkChange = getString("carriernetworkchange") == "show"
+        val roaming = getString("roam") == "show"
+        val name = getString("networkname") ?: "demo mode"
 
         return Mobile(
             level = level,
@@ -107,6 +109,8 @@
             inflateStrength = inflateStrength,
             activity = activity,
             carrierNetworkChange = carrierNetworkChange,
+            roaming = roaming,
+            name = name,
         )
     }
 }
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..8b03f71 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,8 @@
         val inflateStrength: Boolean?,
         @DataActivityType val activity: Int?,
         val carrierNetworkChange: Boolean,
+        val roaming: Boolean,
+        val name: String,
     ) : 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..7e9a9ce 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
@@ -17,29 +17,37 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
 import android.content.Context
+import android.content.IntentFilter
 import android.database.ContentObserver
 import android.provider.Settings.Global
 import android.telephony.CellSignalStrength
 import android.telephony.CellSignalStrengthCdma
 import android.telephony.ServiceState
 import android.telephony.SignalStrength
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.TelephonyCallback
 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.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 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.toMobileDataActivityModel
 import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -61,8 +69,11 @@
 class MobileConnectionRepositoryImpl(
     private val context: Context,
     override val subId: Int,
+    defaultNetworkName: NetworkNameModel,
+    networkNameSeparator: String,
     private val telephonyManager: TelephonyManager,
     private val globalSettings: GlobalSettings,
+    broadcastDispatcher: BroadcastDispatcher,
     defaultDataSubId: StateFlow<Int>,
     globalMobileDataSettingChangedEvent: Flow<Unit>,
     mobileMappingsProxy: MobileMappingsProxy,
@@ -95,7 +106,12 @@
                         TelephonyCallback.CarrierNetworkListener,
                         TelephonyCallback.DisplayInfoListener {
                         override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            state =
+                                state.copy(
+                                    isEmergencyOnly = serviceState.isEmergencyOnly,
+                                    isRoaming = serviceState.roaming,
+                                    operatorAlphaShort = serviceState.operatorAlphaShort,
+                                )
                             trySend(state)
                         }
 
@@ -132,7 +148,10 @@
                         }
 
                         override fun onDataActivity(direction: Int) {
-                            state = state.copy(dataActivityDirection = direction)
+                            state =
+                                state.copy(
+                                    dataActivityDirection = direction.toMobileDataActivityModel()
+                                )
                             trySend(state)
                         }
 
@@ -208,6 +227,24 @@
             globalMobileDataSettingChangedEvent,
         )
 
+    override val cdmaRoaming: StateFlow<Boolean> =
+        telephonyPollingEvent
+            .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val networkName: StateFlow<NetworkNameModel> =
+        broadcastDispatcher
+            .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
+                intent,
+                _ ->
+                if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) {
+                    defaultNetworkName
+                } else {
+                    intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
     override val dataEnabled: StateFlow<Boolean> =
         telephonyPollingEvent
             .mapLatest { dataConnectionAllowed() }
@@ -223,6 +260,7 @@
     class Factory
     @Inject
     constructor(
+        private val broadcastDispatcher: BroadcastDispatcher,
         private val context: Context,
         private val telephonyManager: TelephonyManager,
         private val logger: ConnectivityPipelineLogger,
@@ -233,14 +271,19 @@
     ) {
         fun build(
             subId: Int,
+            defaultNetworkName: NetworkNameModel,
+            networkNameSeparator: String,
             defaultDataSubId: StateFlow<Int>,
             globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
             return MobileConnectionRepositoryImpl(
                 context,
                 subId,
+                defaultNetworkName,
+                networkNameSeparator,
                 telephonyManager.createForSubscriptionId(subId),
                 globalSettings,
+                broadcastDispatcher,
                 defaultDataSubId,
                 globalMobileDataSettingChangedEvent,
                 mobileMappingsProxy,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 483df47..a9b3d18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -38,12 +38,14 @@
 import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
@@ -88,6 +90,14 @@
 ) : MobileConnectionsRepository {
     private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
 
+    private val defaultNetworkName =
+        NetworkNameModel.Default(
+            context.getString(com.android.internal.R.string.lockscreen_carrier_default)
+        )
+
+    private val networkNameSeparator: String =
+        context.getString(R.string.status_bar_network_name_separator)
+
     /**
      * State flow that emits the set of mobile data subscriptions, each represented by its own
      * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
@@ -243,6 +253,8 @@
     private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
         return mobileConnectionRepositoryFactory.build(
             subId,
+            defaultNetworkName,
+            networkNameSeparator,
             defaultDataSubId,
             globalMobileDataSettingChangedEvent,
         )
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..76e6a96a 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
@@ -20,10 +20,13 @@
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.util.CarrierConfigTracker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -32,6 +35,9 @@
 import kotlinx.coroutines.flow.stateIn
 
 interface MobileIconInteractor {
+    /** The current mobile data activity */
+    val activity: Flow<DataActivityModel>
+
     /** Only true if mobile is the default transport but is not validated, otherwise false */
     val isDefaultConnectionFailed: StateFlow<Boolean>
 
@@ -51,9 +57,25 @@
     /** Observable for RAT type (network type) indicator */
     val networkTypeIconGroup: StateFlow<MobileIconGroup>
 
+    /**
+     * Provider name for this network connection. The name can be one of 3 values:
+     * 1. The default network name, if one is configured
+     * 2. A derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED]
+     * 3. Or, in the case where the repository sends us the default network name, we check for an
+     * override in [connectionInfo.operatorAlphaShort], a value that is derived from [ServiceState]
+     */
+    val networkName: StateFlow<NetworkNameModel>
+
     /** 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>
 
@@ -75,10 +97,28 @@
 ) : MobileIconInteractor {
     private val connectionInfo = connectionRepository.connectionInfo
 
+    override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
+
     override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
 
     override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
 
+    override val networkName =
+        combine(connectionInfo, connectionRepository.networkName) { connection, networkName ->
+                if (
+                    networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
+                ) {
+                    NetworkNameModel.Derived(connection.operatorAlphaShort)
+                } else {
+                    networkName
+                }
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                connectionRepository.networkName.value
+            )
+
     /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
     override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
         combine(
@@ -95,6 +135,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..545e624 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
@@ -17,10 +17,12 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.binder
 
 import android.content.res.ColorStateList
+import android.view.View
 import android.view.View.GONE
 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 +31,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
 
@@ -40,9 +41,14 @@
         view: ViewGroup,
         viewModel: MobileIconViewModel,
     ) {
+        val activityContainer = view.requireViewById<View>(R.id.inout_container)
+        val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
+        val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
         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 +70,32 @@
                     }
                 }
 
+                // Set the roaming indicator
+                launch {
+                    viewModel.roaming.distinctUntilChanged().collect { isRoaming ->
+                        roamingView.isVisible = isRoaming
+                        roamingSpace.isVisible = isRoaming
+                    }
+                }
+
+                // Set the activity indicators
+                launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
+
+                launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } }
+
+                launch {
+                    viewModel.activityContainerVisible.collect { activityContainer.isVisible = it }
+                }
+
                 // Set the tint
                 launch {
                     viewModel.tint.collect { tint ->
                         val tintList = ColorStateList.valueOf(tint)
                         iconView.imageTintList = tintList
                         networkTypeView.imageTintList = tintList
+                        roamingView.imageTintList = tintList
+                        activityIn.imageTintList = tintList
+                        activityOut.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..961283f 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
@@ -22,13 +22,16 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+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 kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 
 /**
@@ -48,6 +51,7 @@
     val subscriptionId: Int,
     iconInteractor: MobileIconInteractor,
     logger: ConnectivityPipelineLogger,
+    constants: ConnectivityConstants,
 ) {
     /** Whether or not to show the error state of [SignalDrawable] */
     private val showExclamationMark: Flow<Boolean> =
@@ -87,5 +91,19 @@
             }
         }
 
+    val roaming: Flow<Boolean> = iconInteractor.isRoaming
+
+    private val activity: Flow<DataActivityModel?> =
+        if (!constants.shouldShowActivityConfig) {
+            flowOf(null)
+        } else {
+            iconInteractor.activity
+        }
+
+    val activityInVisible: Flow<Boolean> = activity.map { it?.hasActivityIn ?: false }
+    val activityOutVisible: Flow<Boolean> = activity.map { it?.hasActivityOut ?: false }
+    val activityContainerVisible: Flow<Boolean> =
+        activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+
     val tint: Flow<Int> = flowOf(Color.CYAN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 2349cb7..0b41d31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -20,6 +20,7 @@
 
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import javax.inject.Inject
 import kotlinx.coroutines.InternalCoroutinesApi
@@ -36,13 +37,15 @@
     val subscriptionIdsFlow: StateFlow<List<Int>>,
     private val interactor: MobileIconsInteractor,
     private val logger: ConnectivityPipelineLogger,
+    private val constants: ConnectivityConstants,
 ) {
     /** TODO: do we need to cache these? */
     fun viewModelForSub(subId: Int): MobileIconViewModel =
         MobileIconViewModel(
             subId,
             interactor.createMobileConnectionInteractorForSubId(subId),
-            logger
+            logger,
+            constants,
         )
 
     class Factory
@@ -50,12 +53,14 @@
     constructor(
         private val interactor: MobileIconsInteractor,
         private val logger: ConnectivityPipelineLogger,
+        private val constants: ConnectivityConstants,
     ) {
         fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel {
             return MobileIconsViewModel(
                 subscriptionIdsFlow,
                 interactor,
                 logger,
+                constants,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
index 6efb10f..0c9b86c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.statusbar.pipeline.shared
 
+import android.content.Context
 import android.telephony.TelephonyManager
 import com.android.systemui.Dumpable
+import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
@@ -32,15 +34,25 @@
 @SysUISingleton
 class ConnectivityConstants
 @Inject
-constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable {
+constructor(
+    context: Context,
+    dumpManager: DumpManager,
+    telephonyManager: TelephonyManager,
+) : Dumpable {
     init {
-        dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this)
+        dumpManager.registerNormalDumpable("${SB_LOGGING_TAG}Constants", this)
     }
 
     /** True if this device has the capability for data connections and false otherwise. */
     val hasDataCapabilities = telephonyManager.isDataCapable
 
+    /** True if we should show the activityIn/activityOut icons and false otherwise */
+    val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply { println("hasDataCapabilities=$hasDataCapabilities") }
+        pw.apply {
+            println("hasDataCapabilities=$hasDataCapabilities")
+            println("shouldShowActivityConfig=$shouldShowActivityConfig")
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
new file mode 100644
index 0000000..05d0714
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.statusbar.pipeline.shared.data.model
+
+import android.net.wifi.WifiManager
+import android.telephony.Annotation
+import android.telephony.TelephonyManager
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/** 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 connection has activity out (upload). */
+    val hasActivityOut: Boolean,
+) : Diffable<DataActivityModel> {
+    override fun logDiffs(prevVal: DataActivityModel, row: TableRowLogger) {
+        if (prevVal.hasActivityIn != hasActivityIn) {
+            row.logChange(COL_ACTIVITY_IN, hasActivityIn)
+        }
+        if (prevVal.hasActivityOut != hasActivityOut) {
+            row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
+        }
+    }
+
+    override fun logFull(row: TableRowLogger) {
+        row.logChange(COL_ACTIVITY_IN, hasActivityIn)
+        row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
+    }
+}
+
+const val ACTIVITY_PREFIX = "dataActivity"
+private const val COL_ACTIVITY_IN = "in"
+private const val COL_ACTIVITY_OUT = "out"
+
+fun @receiver:Annotation.DataActivityType Int.toMobileDataActivityModel(): DataActivityModel =
+    when (this) {
+        TelephonyManager.DATA_ACTIVITY_IN ->
+            DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+        TelephonyManager.DATA_ACTIVITY_OUT ->
+            DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+        TelephonyManager.DATA_ACTIVITY_INOUT ->
+            DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+        else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+    }
+
+fun Int.toWifiDataActivityModel(): DataActivityModel =
+    when (this) {
+        WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN ->
+            DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+        WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT ->
+            DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+        WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT ->
+            DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+        else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+    }
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..5ccd6f4 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,9 @@
 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.shared.data.model.toWifiDataActivityModel
 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 +74,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 +230,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 +238,7 @@
                 conflatedCallbackFlow {
                     val callback = TrafficStateCallback { state ->
                         logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
-                        trySend(trafficStateToWifiActivityModel(state))
+                        trySend(state.toWifiDataActivityModel())
                     }
                     wifiManager.registerTrafficStateCallback(mainExecutor, callback)
                     awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
@@ -256,7 +256,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,15 +267,6 @@
         // NetworkCallback inside [wifiNetwork] for our wifi network information.
         val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
 
-        private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
-            return WifiActivityModel(
-                hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
-                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
-                hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
-                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
-            )
-        }
-
         private fun networkCapabilitiesToWifiInfo(
             networkCapabilities: NetworkCapabilities
         ): WifiInfo? {
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/shared/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
deleted file mode 100644
index a4ca41c..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.statusbar.pipeline.wifi.shared.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). */
-    val hasActivityIn: Boolean,
-    /** True if the wifi has activity out (upload). */
-    val hasActivityOut: Boolean,
-) : Diffable<WifiActivityModel> {
-
-    override fun logDiffs(prevVal: WifiActivityModel, row: TableRowLogger) {
-        if (prevVal.hasActivityIn != hasActivityIn) {
-            row.logChange(COL_ACTIVITY_IN, hasActivityIn)
-        }
-        if (prevVal.hasActivityOut != hasActivityOut) {
-            row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
-        }
-    }
-
-    override fun logFull(row: TableRowLogger) {
-        row.logChange(COL_ACTIVITY_IN, hasActivityIn)
-        row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
-    }
-}
-
-const val ACTIVITY_PREFIX = "wifiActivity"
-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/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/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index db7315f..ad48e21 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].
      *
@@ -321,7 +331,7 @@
             return
         }
 
-        removeViewFromWindow(displayInfo)
+        removeViewFromWindow(displayInfo, removalReason)
 
         // Prune anything that's already timed out before determining if we should re-display a
         // different chipbar.
@@ -348,14 +358,14 @@
         removeViewFromWindow(displayInfo)
     }
 
-    private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+    private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
         val view = displayInfo.view
         if (view == null) {
             logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
             return
         }
         displayInfo.view = null // Need other places??
-        animateViewOut(view) {
+        animateViewOut(view, removalReason) {
             windowManager.removeView(view)
             displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
         }
@@ -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].
      */
@@ -405,7 +428,11 @@
      *
      * @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
      */
-    internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    internal open fun animateViewOut(
+        view: ViewGroup,
+        removalReason: String? = null,
+        onAnimationEnd: Runnable
+    ) {
         onAnimationEnd.run()
     }
 
@@ -445,8 +472,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..52980c3 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,
@@ -208,7 +211,7 @@
         )
     }
 
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         val innerView = view.getInnerView()
         innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
         ViewHierarchyAnimator.animateRemoval(
@@ -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/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 9cca950..523cf68 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -159,18 +159,24 @@
         ensureOverlayRemoved()
 
         val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
-        val newView =
-            LightRevealScrim(context, null).apply {
-                revealEffect = createLightRevealEffect()
-                isScrimOpaqueChangedListener = Consumer {}
-                revealAmount =
-                    when (reason) {
-                        FOLD -> TRANSPARENT
-                        UNFOLD -> BLACK
-                    }
-            }
-
         val params = getLayoutParams()
+        val newView =
+            LightRevealScrim(
+                    context,
+                    attrs = null,
+                    initialWidth = params.width,
+                    initialHeight = params.height
+                )
+                .apply {
+                    revealEffect = createLightRevealEffect()
+                    isScrimOpaqueChangedListener = Consumer {}
+                    revealAmount =
+                        when (reason) {
+                            FOLD -> TRANSPARENT
+                            UNFOLD -> BLACK
+                        }
+                }
+
         newRoot.setView(newView, params)
 
         if (onOverlayReady != null) {
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/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index c94c97c..be4bbdf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
 import android.testing.AndroidTestingRunner;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -59,6 +60,8 @@
     @Mock
     DozeParameters mDozeParameters;
     @Mock
+    FeatureFlags mFeatureFlags;
+    @Mock
     ScreenOffAnimationController mScreenOffAnimationController;
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -77,6 +80,7 @@
                 mKeyguardUpdateMonitor,
                 mConfigurationController,
                 mDozeParameters,
+                mFeatureFlags,
                 mScreenOffAnimationController);
     }
 
@@ -96,9 +100,9 @@
     public void setTranslationYExcludingMedia_forwardsCallToView() {
         float translationY = 123f;
 
-        mController.setTranslationYExcludingMedia(translationY);
+        mController.setTranslationY(translationY, /* excludeMedia= */true);
 
-        verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
+        verify(mKeyguardStatusView).setChildrenTranslationY(translationY, /* excludeMedia= */true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index ce44f4d..508aea5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -37,19 +37,23 @@
     fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() {
         val translationY = 1234f
 
-        keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
+        keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true)
 
         assertThat(mediaView.translationY).isEqualTo(0)
-    }
-
-    @Test
-    fun setChildrenTranslationYExcludingMediaView_childrenAreTranslated() {
-        val translationY = 1234f
-
-        keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
 
         childrenExcludingMedia.forEach {
             assertThat(it.translationY).isEqualTo(translationY)
         }
     }
-}
\ No newline at end of file
+
+    @Test
+    fun setChildrenTranslationYIncludeMediaView() {
+        val translationY = 1234f
+
+        keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false)
+
+        statusViewContainer.children.forEach {
+            assertThat(it.translationY).isEqualTo(translationY)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 20d3cd5..d910a27 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
 import android.hardware.display.AmbientDisplayConfiguration;
@@ -76,7 +77,8 @@
 @RunWithLooper
 @SmallTest
 public class DozeSensorsTest extends SysuiTestCase {
-
+    @Mock
+    private Resources mResources;
     @Mock
     private AsyncSensorManager mSensorManager;
     @Mock
@@ -426,7 +428,7 @@
 
     @Test
     public void testGesturesAllInitiallyRespectSettings() {
-        DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
+        DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
                 mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                 mProximitySensor, mFakeSettings, mAuthController,
                 mDevicePostureController, mUserTracker);
@@ -436,9 +438,47 @@
         }
     }
 
+    @Test
+    public void liftToWake_defaultSetting_configDefaultFalse() {
+        // WHEN the default lift to wake gesture setting is false
+        when(mResources.getBoolean(
+                com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(false);
+
+        DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+                mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+                mProximitySensor, mFakeSettings, mAuthController,
+                mDevicePostureController, mUserTracker);
+
+        for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+            // THEN lift to wake's TriggerSensor enabledBySettings is false
+            if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+                assertFalse(sensor.enabledBySetting());
+            }
+        }
+    }
+
+    @Test
+    public void liftToWake_defaultSetting_configDefaultTrue() {
+        // WHEN the default lift to wake gesture setting is true
+        when(mResources.getBoolean(
+                com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(true);
+
+        DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+                mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+                mProximitySensor, mFakeSettings, mAuthController,
+                mDevicePostureController, mUserTracker);
+
+        for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+            // THEN lift to wake's TriggerSensor enabledBySettings is true
+            if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+                assertTrue(sensor.enabledBySetting());
+            }
+        }
+    }
+
     private class TestableDozeSensors extends DozeSensors {
         TestableDozeSensors() {
-            super(mSensorManager, mDozeParameters,
+            super(mResources, mSensorManager, mDozeParameters,
                     mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                     mProximitySensor, mFakeSettings, mAuthController,
                     mDevicePostureController, mUserTracker);
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..c54e456
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.animation.Interpolators.EMPHASIZED_DECELERATE
+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.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_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 3 values should be present, since the dream overlay runs for a small fraction
+            // of the overall animation time
+            assertThat(values.size).isEqualTo(3)
+            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
+                )
+            assertThat(values[2])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0.5f, 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()
+        }
+
+    @Test
+    fun lockscreenFadeIn() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            // Should start running here...
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            // ...up to here
+            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(animValue(0.2f, LOCKSCREEN_ALPHA))
+            assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(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))
+
+            assertThat(values.size).isEqualTo(4)
+            assertThat(values[0])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[1])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[2])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[3])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+
+            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..9c4e849 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,
@@ -63,7 +66,7 @@
         wakeLockBuilder,
         systemClock,
     ) {
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
         onAnimationEnd.run()
     }
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..cefc742 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
@@ -95,6 +98,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
+        whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
 
         fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
         whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
@@ -122,6 +126,7 @@
             fakeExecutor,
             accessibilityManager,
             configurationController,
+            dumpManager,
             powerManager,
             Handler.getMain(),
             mediaTttFlags,
@@ -150,6 +155,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/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 01f13e66..ad993c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -104,6 +104,8 @@
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -286,6 +288,8 @@
     @Mock private ViewTreeObserver mViewTreeObserver;
     @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     @Mock private MotionEvent mDownMotionEvent;
     @Captor
     private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -502,6 +506,8 @@
                 systemClock,
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
+                mDreamingToLockscreenTransitionViewModel,
+                mKeyguardTransitionInteractor,
                 mDumpManager);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
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/LightRevealScrimTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
index 97fe25d..d3befb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
@@ -20,6 +20,7 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -36,7 +37,7 @@
 
   @Before
   fun setUp() {
-    scrim = LightRevealScrim(context, null)
+    scrim = LightRevealScrim(context, null, DEFAULT_WIDTH, DEFAULT_HEIGHT)
     scrim.isScrimOpaqueChangedListener = Consumer { opaque ->
       isOpaque = opaque
     }
@@ -63,4 +64,25 @@
     scrim.revealAmount = 0.5f
     assertFalse("Scrim is opaque even though it's revealed", scrim.isScrimOpaque)
   }
+
+  @Test
+  fun testBeforeOnMeasure_defaultDimensions() {
+    assertThat(scrim.viewWidth).isEqualTo(DEFAULT_WIDTH)
+    assertThat(scrim.viewHeight).isEqualTo(DEFAULT_HEIGHT)
+  }
+
+  @Test
+  fun testAfterOnMeasure_measuredDimensions() {
+    scrim.measure(/* widthMeasureSpec= */ exact(1), /* heightMeasureSpec= */ exact(2))
+
+    assertThat(scrim.viewWidth).isEqualTo(1)
+    assertThat(scrim.viewHeight).isEqualTo(2)
+  }
+
+  private fun exact(value: Int) = View.MeasureSpec.makeMeasureSpec(value, View.MeasureSpec.EXACTLY)
+
+  private companion object {
+    private const val DEFAULT_WIDTH = 42
+    private const val DEFAULT_HEIGHT = 24
+  }
 }
\ No newline at end of file
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..59eec53 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
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
 // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
@@ -30,6 +31,11 @@
     private val _isDefaultDataSubscription = MutableStateFlow(true)
     override val isDefaultDataSubscription = _isDefaultDataSubscription
 
+    override val cdmaRoaming = MutableStateFlow(false)
+
+    override val networkName =
+        MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default"))
+
     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..3d5316d 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
@@ -18,13 +18,16 @@
 
 import android.telephony.Annotation
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
 import androidx.test.filters.SmallTest
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -95,6 +98,8 @@
                     inflateStrength = testCase.inflateStrength,
                     activity = testCase.activity,
                     carrierNetworkChange = testCase.carrierNetworkChange,
+                    roaming = testCase.roaming,
+                    name = "demo name",
                 )
 
             fakeNetworkEventFlow.value = networkModel
@@ -113,9 +118,12 @@
                 assertThat(conn.subId).isEqualTo(model.subId)
                 assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
                 assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
-                assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+                assertThat(connectionInfo.dataActivityDirection)
+                    .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
+                assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -138,6 +146,8 @@
         val inflateStrength: Boolean,
         @Annotation.DataActivityType val activity: Int,
         val carrierNetworkChange: Boolean,
+        val roaming: Boolean,
+        val name: String,
     ) {
         override fun toString(): String {
             return "INPUT(level=$level, " +
@@ -146,7 +156,9 @@
                 "carrierId=$carrierId, " +
                 "inflateStrength=$inflateStrength, " +
                 "activity=$activity, " +
-                "carrierNetworkChange=$carrierNetworkChange)"
+                "carrierNetworkChange=$carrierNetworkChange, " +
+                "roaming=$roaming, " +
+                "name=$name)"
         }
 
         // Convenience for iterating test data and creating new cases
@@ -158,6 +170,8 @@
             inflateStrength: Boolean? = null,
             @Annotation.DataActivityType activity: Int? = null,
             carrierNetworkChange: Boolean? = null,
+            roaming: Boolean? = null,
+            name: String? = null,
         ): TestCase =
             TestCase(
                 level = level ?: this.level,
@@ -166,7 +180,9 @@
                 carrierId = carrierId ?: this.carrierId,
                 inflateStrength = inflateStrength ?: this.inflateStrength,
                 activity = activity ?: this.activity,
-                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
+                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
+                roaming = roaming ?: this.roaming,
+                name = name ?: this.name,
             )
     }
 
@@ -193,6 +209,9 @@
                 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)
+        private val names = listOf("name 1", "name 2")
 
         @Parameters(name = "{0}") @JvmStatic fun data() = testData()
 
@@ -226,7 +245,9 @@
                     carrierIds.first(),
                     inflateStrength.first(),
                     activity.first(),
-                    carrierNetworkChange.first()
+                    carrierNetworkChange.first(),
+                    roaming.first(),
+                    names.first(),
                 )
 
             val tail =
@@ -237,6 +258,8 @@
                         inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
                         activity.map { baseCase.modifiedBy(activity = it) },
                         carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
+                        roaming.map { baseCase.modifiedBy(roaming = it) },
+                        names.map { baseCase.modifiedBy(name = 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..34f30eb 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
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
 
 import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
 import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
 import androidx.test.filters.SmallTest
 import com.android.settingslib.SignalIcon
@@ -24,9 +25,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -289,9 +292,12 @@
                 assertThat(conn.subId).isEqualTo(model.subId)
                 assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
                 assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
-                assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+                assertThat(connectionInfo.dataActivityDirection)
+                    .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
+                assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -313,6 +319,7 @@
     inflateStrength: Boolean? = false,
     activity: Int? = null,
     carrierNetworkChange: Boolean = false,
+    roaming: Boolean = false,
 ): FakeNetworkEventModel =
     FakeNetworkEventModel.Mobile(
         level = level,
@@ -322,4 +329,6 @@
         inflateStrength = inflateStrength,
         activity = activity,
         carrierNetworkChange = carrierNetworkChange,
+        roaming = roaming,
+        name = "demo name",
     )
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..7fa8065 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
+import android.content.Intent
 import android.os.UserHandle
 import android.provider.Settings
 import android.telephony.CellSignalStrengthCdma
@@ -23,27 +24,44 @@
 import android.telephony.SignalStrength
 import android.telephony.SubscriptionInfo
 import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.DataActivityListener
 import android.telephony.TelephonyCallback.ServiceStateListener
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
+import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
 import android.telephony.TelephonyManager.DATA_CONNECTED
 import android.telephony.TelephonyManager.DATA_CONNECTING
 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.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import android.telephony.TelephonyManager.EXTRA_SPN
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
@@ -88,8 +106,11 @@
             MobileConnectionRepositoryImpl(
                 context,
                 SUB_1_ID,
+                DEFAULT_NAME,
+                SEP,
                 telephonyManager,
                 globalSettings,
+                fakeBroadcastDispatcher,
                 connectionsRepo.defaultDataSubId,
                 connectionsRepo.globalMobileDataSettingChangedEvent,
                 mobileMappings,
@@ -247,10 +268,11 @@
             var latest: MobileConnectionModel? = null
             val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
 
-            val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
-            callback.onDataActivity(3)
+            val callback = getTelephonyCallbackForType<DataActivityListener>()
+            callback.onDataActivity(DATA_ACTIVITY_INOUT)
 
-            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+            assertThat(latest?.dataActivityDirection)
+                .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel())
 
             job.cancel()
         }
@@ -402,6 +424,167 @@
             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()
+        }
+
+    @Test
+    fun `activity - updates from callback`() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job =
+                underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this)
+
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+            val cb = getTelephonyCallbackForType<DataActivityListener>()
+            cb.onDataActivity(DATA_ACTIVITY_IN)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+            cb.onDataActivity(DATA_ACTIVITY_OUT)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+            cb.onDataActivity(DATA_ACTIVITY_INOUT)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+            cb.onDataActivity(DATA_ACTIVITY_NONE)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+            cb.onDataActivity(DATA_ACTIVITY_DORMANT)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+            cb.onDataActivity(1234)
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - uses broadcast info - returns derived`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val intent = spnIntent()
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intent)
+            }
+
+            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - broadcast not for this sub id - returns default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val intent = spnIntent(subId = 101)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intent)
+            }
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - operatorAlphaShort - tracked`() =
+        runBlocking(IMMEDIATE) {
+            var latest: String? = null
+
+            val job =
+                underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this)
+
+            val shortName = "short name"
+            val serviceState = ServiceState()
+            serviceState.setOperatorName(
+                /* longName */ "long name",
+                /* shortName */ shortName,
+                /* numeric */ "12345",
+            )
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest).isEqualTo(shortName)
+
+            job.cancel()
+        }
+
     private fun getTelephonyCallbacks(): List<TelephonyCallback> {
         val callbackCaptor = argumentCaptor<TelephonyCallback>()
         Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
@@ -427,10 +610,31 @@
         return signalStrength
     }
 
+    private fun spnIntent(
+        subId: Int = SUB_1_ID,
+        showSpn: Boolean = true,
+        spn: String = SPN,
+        showPlmn: Boolean = true,
+        plmn: String = PLMN,
+    ): Intent =
+        Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply {
+            putExtra(EXTRA_SUBSCRIPTION_ID, subId)
+            putExtra(EXTRA_SHOW_SPN, showSpn)
+            putExtra(EXTRA_SPN, spn)
+            putExtra(EXTRA_SHOW_PLMN, showPlmn)
+            putExtra(EXTRA_PLMN, plmn)
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+        private const val SEP = "-"
+
+        private const val SPN = "testSpn"
+        private const val PLMN = "testPlmn"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6d80acb..3cc1e8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -91,6 +91,7 @@
 
         connectionFactory =
             MobileConnectionRepositoryImpl.Factory(
+                fakeBroadcastDispatcher,
                 context = context,
                 telephonyManager = telephonyManager,
                 bgDispatcher = IMMEDIATE,
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..c3519b7 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
@@ -19,17 +19,31 @@
 import android.telephony.CellSignalStrength
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeMobileIconInteractor : MobileIconInteractor {
     override val alwaysShowDataRatIcon = MutableStateFlow(false)
 
+    override val activity =
+        MutableStateFlow(
+            DataActivityModel(
+                hasActivityIn = false,
+                hasActivityOut = false,
+            )
+        )
+
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
     override val networkTypeIconGroup = _iconGroup
 
+    override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo mode"))
+
     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..4dca780 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
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -298,6 +299,135 @@
             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()
+        }
+
+    @Test
+    fun `network name - uses operatorAlphaShot when non null and repo is default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val testOperatorName = "operatorAlphaShort"
+
+            // Default network name, operator name is non-null, uses the operator name
+            connectionRepository.networkName.value = DEFAULT_NAME
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(operatorAlphaShort = testOperatorName)
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName))
+
+            // Default network name, operator name is null, uses the default
+            connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null))
+            yield()
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            // Derived network name, operator name non-null, uses the derived name
+            connectionRepository.networkName.value = DERIVED_NAME
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(operatorAlphaShort = testOperatorName)
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(DERIVED_NAME)
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -307,5 +437,8 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
+        private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
     }
 }
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..415ce75 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
@@ -23,7 +23,10 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+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.DataActivityModel
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
@@ -40,6 +43,7 @@
     private lateinit var underTest: MobileIconViewModel
     private val interactor = FakeMobileIconInteractor()
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var constants: ConnectivityConstants
 
     @Before
     fun setUp() {
@@ -53,7 +57,7 @@
             setNumberOfLevels(4)
             isDataConnected.value = true
         }
-        underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
+        underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants)
     }
 
     @Test
@@ -234,6 +238,108 @@
             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()
+        }
+
+    @Test
+    fun `data activity - null when config is off`() =
+        runBlocking(IMMEDIATE) {
+            // Create a new view model here so the constants are properly read
+            whenever(constants.shouldShowActivityConfig).thenReturn(false)
+            underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants)
+
+            var inVisible: Boolean? = null
+            val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+            var outVisible: Boolean? = null
+            val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this)
+
+            var containerVisible: Boolean? = null
+            val containerJob =
+                underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
+
+            interactor.activity.value =
+                DataActivityModel(
+                    hasActivityIn = true,
+                    hasActivityOut = true,
+                )
+
+            assertThat(inVisible).isFalse()
+            assertThat(outVisible).isFalse()
+            assertThat(containerVisible).isFalse()
+
+            inJob.cancel()
+            outJob.cancel()
+            containerJob.cancel()
+        }
+
+    @Test
+    fun `data activity - config on - test indicators`() =
+        runBlocking(IMMEDIATE) {
+            // Create a new view model here so the constants are properly read
+            whenever(constants.shouldShowActivityConfig).thenReturn(true)
+            underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants)
+
+            var inVisible: Boolean? = null
+            val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+            var outVisible: Boolean? = null
+            val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+            var containerVisible: Boolean? = null
+            val containerJob =
+                underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+            interactor.activity.value =
+                DataActivityModel(
+                    hasActivityIn = true,
+                    hasActivityOut = false,
+                )
+
+            yield()
+
+            assertThat(inVisible).isTrue()
+            assertThat(outVisible).isFalse()
+            assertThat(containerVisible).isTrue()
+
+            interactor.activity.value =
+                DataActivityModel(
+                    hasActivityIn = false,
+                    hasActivityOut = true,
+                )
+
+            assertThat(inVisible).isFalse()
+            assertThat(outVisible).isTrue()
+            assertThat(containerVisible).isTrue()
+
+            interactor.activity.value =
+                DataActivityModel(
+                    hasActivityIn = false,
+                    hasActivityOut = false,
+                )
+
+            assertThat(inVisible).isFalse()
+            assertThat(outVisible).isFalse()
+            assertThat(containerVisible).isFalse()
+
+            inJob.cancel()
+            outJob.cancel()
+            containerJob.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/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..4ef4e6c 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,
@@ -61,7 +64,7 @@
         wakeLockBuilder,
         systemClock,
     ) {
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
         onAnimationEnd.run()
     }
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
index f26404c6..da4dfa9 100644
--- a/proto/src/windowmanager.proto
+++ b/proto/src/windowmanager.proto
@@ -68,4 +68,8 @@
   LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
   // Represents the current vertical position for the letterboxed activity
   LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+  // Represents the current horizontal position for the letterboxed activity in book mode
+  LetterboxHorizontalReachability letterbox_position_for_book_mode_reachability = 3;
+  // Represents the current vertical position for the letterboxed activity in tabletop mode
+  LetterboxVerticalReachability letterbox_position_for_tabletop_mode_reachability = 4;
 }
\ No newline at end of file
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/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index fa6f4ee..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,15 +1470,9 @@
         }
 
         // Then make sure none of the activities have more than the max number of shortcuts.
-        int total = 0;
         for (int i = counts.size() - 1; i >= 0; i--) {
-            int count = counts.valueAt(i);
-            service.enforceMaxActivityShortcuts(count);
-            total += count;
+            service.enforceMaxActivityShortcuts(counts.valueAt(i));
         }
-
-        // Finally make sure that the app doesn't have more than the max number of shortcuts.
-        service.enforceMaxAppShortcuts(total);
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 014a77b..0b20683 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,9 +181,6 @@
     static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
 
     @VisibleForTesting
-    static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
-
-    @VisibleForTesting
     static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
 
     @VisibleForTesting
@@ -260,11 +257,6 @@
         String KEY_MAX_SHORTCUTS = "max_shortcuts";
 
         /**
-         * Key name for the max dynamic shortcuts per app. (int)
-         */
-        String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
-
-        /**
          * Key name for icon compression quality, 0-100.
          */
         String KEY_ICON_QUALITY = "icon_quality";
@@ -337,14 +329,9 @@
             new SparseArray<>();
 
     /**
-     * Max number of dynamic + manifest shortcuts that each activity can have at a time.
-     */
-    private int mMaxShortcutsPerActivity;
-
-    /**
      * Max number of dynamic + manifest shortcuts that each application can have at a time.
      */
-    private int mMaxShortcutsPerApp;
+    private int mMaxShortcuts;
 
     /**
      * Max number of updating API calls that each application can make during the interval.
@@ -817,12 +804,9 @@
         mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
                 ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
 
-        mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
+        mMaxShortcuts = Math.max(0, (int) parser.getLong(
                 ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
 
-        mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
-                ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
-
         final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
                 ? (int) parser.getLong(
                 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1762,33 +1746,16 @@
      *                                  {@link #getMaxActivityShortcuts()}.
      */
     void enforceMaxActivityShortcuts(int numShortcuts) {
-        if (numShortcuts > mMaxShortcutsPerActivity) {
+        if (numShortcuts > mMaxShortcuts) {
             throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
         }
     }
 
     /**
-     * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
-     *                                  {@link #getMaxAppShortcuts()}.
-     */
-    void enforceMaxAppShortcuts(int numShortcuts) {
-        if (numShortcuts > mMaxShortcutsPerApp) {
-            throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
-        }
-    }
-
-    /**
      * Return the max number of dynamic + manifest shortcuts for each launcher icon.
      */
     int getMaxActivityShortcuts() {
-        return mMaxShortcutsPerActivity;
-    }
-
-    /**
-     * Return the max number of dynamic + manifest shortcuts for each launcher icon.
-     */
-    int getMaxAppShortcuts() {
-        return mMaxShortcutsPerApp;
+        return mMaxShortcuts;
     }
 
     /**
@@ -2221,8 +2188,6 @@
             ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
             fillInDefaultActivity(Arrays.asList(shortcut));
 
-            enforceMaxAppShortcuts(ps.getShortcutCount());
-
             if (!shortcut.hasRank()) {
                 shortcut.setRank(0);
             }
@@ -2610,7 +2575,7 @@
             throws RemoteException {
         verifyCaller(packageName, userId);
 
-        return mMaxShortcutsPerActivity;
+        return mMaxShortcuts;
     }
 
     @Override
@@ -4758,7 +4723,7 @@
                 pw.print("    maxUpdatesPerInterval: ");
                 pw.println(mMaxUpdatesPerInterval);
                 pw.print("    maxShortcutsPerActivity: ");
-                pw.println(mMaxShortcutsPerActivity);
+                pw.println(mMaxShortcuts);
                 pw.println();
 
                 mStatLogger.dump(pw, "  ");
@@ -5245,7 +5210,7 @@
 
     @VisibleForTesting
     int getMaxShortcutsForTest() {
-        return mMaxShortcutsPerActivity;
+        return mMaxShortcuts;
     }
 
     @VisibleForTesting
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/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 418e1ed..57eeb9a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8149,7 +8149,7 @@
     }
 
     /**
-     * Adjusts position of resolved bounds if they doesn't fill the parent using gravity
+     * Adjusts position of resolved bounds if they don't fill the parent using gravity
      * requested in the config or via an ADB command. For more context see {@link
      * LetterboxUiController#getHorizontalPositionMultiplier(Configuration)} and
      * {@link LetterboxUiController#getVerticalPositionMultiplier(Configuration)}
@@ -8168,9 +8168,8 @@
         int offsetX = 0;
         if (parentBounds.width() != screenResolvedBounds.width()) {
             if (screenResolvedBounds.width() <= parentAppBounds.width()) {
-                float positionMultiplier =
-                        mLetterboxUiController.getHorizontalPositionMultiplier(
-                                newParentConfiguration);
+                float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
+                        newParentConfiguration);
                 offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
                         * positionMultiplier);
             }
@@ -8180,9 +8179,8 @@
         int offsetY = 0;
         if (parentBounds.height() != screenResolvedBounds.height()) {
             if (screenResolvedBounds.height() <= parentAppBounds.height()) {
-                float positionMultiplier =
-                        mLetterboxUiController.getVerticalPositionMultiplier(
-                                newParentConfiguration);
+                float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
+                        newParentConfiguration);
                 offsetY = (int) Math.ceil((parentAppBounds.height() - screenResolvedBounds.height())
                         * positionMultiplier);
             }
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..34bdb7a 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.util.RotationUtils.deltaRotation;
@@ -55,9 +56,11 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.ArraySet;
 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;
@@ -75,6 +78,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
+import java.util.Set;
 
 /**
  * Defines the mapping between orientation and rotation of a display.
@@ -211,15 +215,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 +240,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 +255,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);
@@ -1521,6 +1555,15 @@
         proto.end(token);
     }
 
+    boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+        if (mFoldController == null) return false;
+        return mFoldController.isDeviceInPosture(state, isTabletop);
+    }
+
+    boolean isDisplaySeparatingHinge() {
+        return mFoldController != null && mFoldController.isSeparatingHinge();
+    }
+
     /**
      * Called by the DeviceStateManager callback when the device state changes.
      */
@@ -1538,6 +1581,63 @@
         private DeviceStateController.FoldState mFoldState =
                 DeviceStateController.FoldState.UNKNOWN;
         private boolean mInHalfFoldTransition = false;
+        private final boolean mIsDisplayAlwaysSeparatingHinge;
+        private final Set<Integer> mTabletopRotations;
+
+        FoldController() {
+            mTabletopRotations = new ArraySet<>();
+            int[] tabletop_rotations = mContext.getResources().getIntArray(
+                    R.array.config_deviceTabletopRotations);
+            if (tabletop_rotations != null) {
+                for (int angle : tabletop_rotations) {
+                    switch (angle) {
+                        case 0:
+                            mTabletopRotations.add(Surface.ROTATION_0);
+                            break;
+                        case 90:
+                            mTabletopRotations.add(Surface.ROTATION_90);
+                            break;
+                        case 180:
+                            mTabletopRotations.add(Surface.ROTATION_180);
+                            break;
+                        case 270:
+                            mTabletopRotations.add(Surface.ROTATION_270);
+                            break;
+                        default:
+                            ProtoLog.e(WM_DEBUG_ORIENTATION,
+                                    "Invalid surface rotation angle in "
+                                            + "config_deviceTabletopRotations: %d",
+                                    angle);
+                    }
+                }
+            } else {
+                ProtoLog.w(WM_DEBUG_ORIENTATION,
+                        "config_deviceTabletopRotations is not defined. Half-fold "
+                                + "letterboxing will work inconsistently.");
+            }
+            mIsDisplayAlwaysSeparatingHinge = mContext.getResources().getBoolean(
+                    R.bool.config_isDisplayHingeAlwaysSeparating);
+        }
+
+        boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+            if (state != mFoldState) {
+                return false;
+            }
+            if (mFoldState == DeviceStateController.FoldState.HALF_FOLDED) {
+                return !(isTabletop ^ mTabletopRotations.contains(mRotation));
+            }
+            return true;
+        }
+
+        DeviceStateController.FoldState getFoldState() {
+            return mFoldState;
+        }
+
+        boolean isSeparatingHinge() {
+            return mFoldState == DeviceStateController.FoldState.HALF_FOLDED
+                    || (mFoldState == DeviceStateController.FoldState.OPEN
+                        && mIsDisplayAlwaysSeparatingHinge);
+        }
 
         boolean overrideFrozenRotation() {
             return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
@@ -1586,6 +1686,15 @@
                 mService.updateRotation(false /* alwaysSendConfiguration */,
                         false /* forceRelayout */);
             }
+            // Alert the activity of possible new bounds.
+            final Task topFullscreenTask =
+                    mDisplayContent.getTask(t -> t.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+            if (topFullscreenTask != null) {
+                final ActivityRecord top = topFullscreenTask.topRunningActivity();
+                if (top != null) {
+                    top.recomputeConfiguration();
+                }
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 127a7bf..3eca364 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -16,12 +16,16 @@
 
 package com.android.server.wm;
 
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 import android.provider.DeviceConfig;
+import android.util.Slog;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +37,8 @@
 /** Reads letterbox configs from resources and controls their overrides at runtime. */
 final class LetterboxConfiguration {
 
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
+
     /**
      * Override of aspect ratio for fixed orientation letterboxing that is set via ADB with
      * set-fixed-orientation-letterbox-aspect-ratio or via {@link
@@ -144,6 +150,14 @@
     // side of the screen and 1.0 to the bottom side.
     private float mLetterboxVerticalPositionMultiplier;
 
+    // Horizontal position of a center of the letterboxed app window when the device is half-folded.
+    // 0 corresponds to the left side of the screen and 1.0 to the right side.
+    private float mLetterboxBookModePositionMultiplier;
+
+    // Vertical position of a center of the letterboxed app window when the device is half-folded.
+    // 0 corresponds to the top side of the screen and 1.0 to the bottom side.
+    private float mLetterboxTabletopModePositionMultiplier;
+
     // Default horizontal position the letterboxed app window when horizontal reachability is
     // enabled and an app is fullscreen in landscape device orientation.
     // It is used as a starting point for mLetterboxPositionForHorizontalReachability.
@@ -179,8 +193,15 @@
 
     LetterboxConfiguration(Context systemUiContext) {
         this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
-                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
-                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+                        /* forBookMode */ false),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+                        /* forTabletopMode */ false),
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+                        /* forBookMode */ true),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+                        /* forTabletopMode */ true)
+        ));
     }
 
     @VisibleForTesting
@@ -200,14 +221,18 @@
                 R.dimen.config_letterboxHorizontalPositionMultiplier);
         mLetterboxVerticalPositionMultiplier = mContext.getResources().getFloat(
                 R.dimen.config_letterboxVerticalPositionMultiplier);
+        mLetterboxBookModePositionMultiplier = mContext.getResources().getFloat(
+                R.dimen.config_letterboxBookModePositionMultiplier);
+        mLetterboxTabletopModePositionMultiplier = mContext.getResources().getFloat(
+                R.dimen.config_letterboxTabletopModePositionMultiplier);
         mIsHorizontalReachabilityEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsHorizontalReachabilityEnabled);
         mIsVerticalReachabilityEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsVerticalReachabilityEnabled);
         mDefaultPositionForHorizontalReachability =
-                readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+                readLetterboxHorizontalReachabilityPositionFromConfig(mContext, false);
         mDefaultPositionForVerticalReachability =
-                readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+                readLetterboxVerticalReachabilityPositionFromConfig(mContext, false);
         mIsEducationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEducationEnabled);
         setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
@@ -460,11 +485,30 @@
      * or via an ADB command. 0 corresponds to the left side of the screen and 1 to the
      * right side.
      */
-    float getLetterboxHorizontalPositionMultiplier() {
-        return (mLetterboxHorizontalPositionMultiplier < 0.0f
-                || mLetterboxHorizontalPositionMultiplier > 1.0f)
-                        // Default to central position if invalid value is provided.
-                        ? 0.5f : mLetterboxHorizontalPositionMultiplier;
+    float getLetterboxHorizontalPositionMultiplier(boolean isInBookMode) {
+        if (isInBookMode) {
+            if (mLetterboxBookModePositionMultiplier < 0.0f
+                    || mLetterboxBookModePositionMultiplier > 1.0f) {
+                Slog.w(TAG,
+                        "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=true): "
+                        + mLetterboxBookModePositionMultiplier);
+                // Default to left position if invalid value is provided.
+                return 0.0f;
+            } else {
+                return mLetterboxBookModePositionMultiplier;
+            }
+        } else {
+            if (mLetterboxHorizontalPositionMultiplier < 0.0f
+                    || mLetterboxHorizontalPositionMultiplier > 1.0f) {
+                Slog.w(TAG,
+                        "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=false):"
+                        + mLetterboxBookModePositionMultiplier);
+                // Default to central position if invalid value is provided.
+                return 0.5f;
+            } else {
+                return mLetterboxHorizontalPositionMultiplier;
+            }
+        }
     }
 
     /*
@@ -473,11 +517,18 @@
      * or via an ADB command. 0 corresponds to the top side of the screen and 1 to the
      * bottom side.
      */
-    float getLetterboxVerticalPositionMultiplier() {
-        return (mLetterboxVerticalPositionMultiplier < 0.0f
-                || mLetterboxVerticalPositionMultiplier > 1.0f)
-                        // Default to central position if invalid value is provided.
-                        ? 0.5f : mLetterboxVerticalPositionMultiplier;
+    float getLetterboxVerticalPositionMultiplier(boolean isInTabletopMode) {
+        if (isInTabletopMode) {
+            return (mLetterboxTabletopModePositionMultiplier < 0.0f
+                    || mLetterboxTabletopModePositionMultiplier > 1.0f)
+                    // Default to top position if invalid value is provided.
+                    ? 0.0f : mLetterboxTabletopModePositionMultiplier;
+        } else {
+            return (mLetterboxVerticalPositionMultiplier < 0.0f
+                    || mLetterboxVerticalPositionMultiplier > 1.0f)
+                    // Default to central position if invalid value is provided.
+                    ? 0.5f : mLetterboxVerticalPositionMultiplier;
+        }
     }
 
     /**
@@ -618,7 +669,8 @@
      */
     void resetDefaultPositionForHorizontalReachability() {
         mDefaultPositionForHorizontalReachability =
-                readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+                readLetterboxHorizontalReachabilityPositionFromConfig(mContext,
+                        false /* forBookMode */);
     }
 
     /**
@@ -627,27 +679,34 @@
      */
     void resetDefaultPositionForVerticalReachability() {
         mDefaultPositionForVerticalReachability =
-                readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+                readLetterboxVerticalReachabilityPositionFromConfig(mContext,
+                        false /* forTabletopMode */);
     }
 
     @LetterboxHorizontalReachabilityPosition
-    private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context) {
+    private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context,
+            boolean forBookMode) {
         int position = context.getResources().getInteger(
-                R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+                forBookMode
+                    ? R.integer.config_letterboxDefaultPositionForBookModeReachability
+                    : R.integer.config_letterboxDefaultPositionForHorizontalReachability);
         return position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT
-                    || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
-                    || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
+                || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
                     ? position : LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
     }
 
     @LetterboxVerticalReachabilityPosition
-    private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context) {
+    private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context,
+            boolean forTabletopMode) {
         int position = context.getResources().getInteger(
-                R.integer.config_letterboxDefaultPositionForVerticalReachability);
+                forTabletopMode
+                    ? R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+                    : R.integer.config_letterboxDefaultPositionForVerticalReachability);
         return position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP
                 || position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
                 || position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM
-                ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+                    ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
     }
 
     /*
@@ -656,9 +715,10 @@
      *
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
-    float getHorizontalMultiplierForReachability() {
+    float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) {
         final int letterboxPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        isDeviceInBookMode);
         switch (letterboxPositionForHorizontalReachability) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
                 return 0.0f;
@@ -679,9 +739,10 @@
      *
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
-    float getVerticalMultiplierForReachability() {
+    float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) {
         final int letterboxPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+                        isDeviceInTabletopMode);
         switch (letterboxPositionForVerticalReachability) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
                 return 0.0f;
@@ -701,8 +762,9 @@
      * enabled.
      */
     @LetterboxHorizontalReachabilityPosition
-    int getLetterboxPositionForHorizontalReachability() {
-        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+    int getLetterboxPositionForHorizontalReachability(boolean isInFullScreenBookMode) {
+        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                isInFullScreenBookMode);
     }
 
     /*
@@ -710,8 +772,9 @@
      * enabled.
      */
     @LetterboxVerticalReachabilityPosition
-    int getLetterboxPositionForVerticalReachability() {
-        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+    int getLetterboxPositionForVerticalReachability(boolean isInFullScreenTabletopMode) {
+        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+                isInFullScreenTabletopMode);
     }
 
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -750,34 +813,41 @@
      * Changes letterbox position for horizontal reachability to the next available one on the
      * right side.
      */
-    void movePositionForHorizontalReachabilityToNextRightStop() {
-        updatePositionForHorizontalReachability(prev -> Math.min(
-                prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
+    void movePositionForHorizontalReachabilityToNextRightStop(boolean isDeviceInBookMode) {
+        updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.min(
+                prev + (isDeviceInBookMode ? 2 : 1), // Move 2 stops in book mode to avoid center.
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
     }
 
     /**
      * Changes letterbox position for horizontal reachability to the next available one on the left
      * side.
      */
-    void movePositionForHorizontalReachabilityToNextLeftStop() {
-        updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
+    void movePositionForHorizontalReachabilityToNextLeftStop(boolean isDeviceInBookMode) {
+        updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.max(
+                prev - (isDeviceInBookMode ? 2 : 1), 0)); // Move 2 stops in book mode to avoid
+                                                          // center.
     }
 
     /**
      * Changes letterbox position for vertical reachability to the next available one on the bottom
      * side.
      */
-    void movePositionForVerticalReachabilityToNextBottomStop() {
-        updatePositionForVerticalReachability(prev -> Math.min(
-                prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
+    void movePositionForVerticalReachabilityToNextBottomStop(boolean isDeviceInTabletopMode) {
+        updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.min(
+                prev + (isDeviceInTabletopMode ? 2 : 1), // Move 2 stops in tabletop mode to avoid
+                                                         // center.
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
     }
 
     /**
      * Changes letterbox position for vertical reachability to the next available one on the top
      * side.
      */
-    void movePositionForVerticalReachabilityToNextTopStop() {
-        updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
+    void movePositionForVerticalReachabilityToNextTopStop(boolean isDeviceInTabletopMode) {
+        updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.max(
+                prev - (isDeviceInTabletopMode ? 2 : 1), 0)); // Move 2 stops in tabletop mode to
+                                                              // avoid center.
     }
 
     /**
@@ -854,25 +924,27 @@
     }
 
     /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
-    private void updatePositionForHorizontalReachability(
+    private void updatePositionForHorizontalReachability(boolean isDeviceInBookMode,
             Function<Integer, Integer> newHorizonalPositionFun) {
         final int letterboxPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        isDeviceInBookMode);
         final int nextHorizontalPosition = newHorizonalPositionFun.apply(
                 letterboxPositionForHorizontalReachability);
         mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
-                nextHorizontalPosition);
+                isDeviceInBookMode, nextHorizontalPosition);
     }
 
     /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
-    private void updatePositionForVerticalReachability(
+    private void updatePositionForVerticalReachability(boolean isDeviceInTabletopMode,
             Function<Integer, Integer> newVerticalPositionFun) {
         final int letterboxPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+                        isDeviceInTabletopMode);
         final int nextVerticalPosition = newVerticalPositionFun.apply(
                 letterboxPositionForVerticalReachability);
         mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
-                nextVerticalPosition);
+                isDeviceInTabletopMode, nextVerticalPosition);
     }
 
     // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
index 70639b1..4a99db5 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -55,6 +55,8 @@
     private final Context mContext;
     private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
     private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+    private final Supplier<Integer> mDefaultBookModeReachabilitySupplier;
+    private final Supplier<Integer> mDefaultTabletopModeReachabilitySupplier;
 
     // Horizontal position of a center of the letterboxed app window which is global to prevent
     // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
@@ -64,6 +66,11 @@
     @LetterboxHorizontalReachabilityPosition
     private volatile int mLetterboxPositionForHorizontalReachability;
 
+    // The same as mLetterboxPositionForHorizontalReachability but used when the device is
+    // half-folded.
+    @LetterboxHorizontalReachabilityPosition
+    private volatile int mLetterboxPositionForBookModeReachability;
+
     // Vertical position of a center of the letterboxed app window which is global to prevent
     // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
     // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
@@ -72,6 +79,11 @@
     @LetterboxVerticalReachabilityPosition
     private volatile int mLetterboxPositionForVerticalReachability;
 
+    // The same as mLetterboxPositionForVerticalReachability but used when the device is
+    // half-folded.
+    @LetterboxVerticalReachabilityPosition
+    private volatile int mLetterboxPositionForTabletopModeReachability;
+
     @NonNull
     private final AtomicFile mConfigurationFile;
 
@@ -83,9 +95,13 @@
 
     LetterboxConfigurationPersister(Context systemUiContext,
             Supplier<Integer> defaultHorizontalReachabilitySupplier,
-            Supplier<Integer> defaultVerticalReachabilitySupplier) {
+            Supplier<Integer> defaultVerticalReachabilitySupplier,
+            Supplier<Integer> defaultBookModeReachabilitySupplier,
+            Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
         this(systemUiContext, defaultHorizontalReachabilitySupplier,
                 defaultVerticalReachabilitySupplier,
+                defaultBookModeReachabilitySupplier,
+                defaultTabletopModeReachabilitySupplier,
                 Environment.getDataSystemDirectory(), new PersisterQueue(),
                 /* completionCallback */ null);
     }
@@ -93,11 +109,18 @@
     @VisibleForTesting
     LetterboxConfigurationPersister(Context systemUiContext,
             Supplier<Integer> defaultHorizontalReachabilitySupplier,
-            Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+            Supplier<Integer> defaultVerticalReachabilitySupplier,
+            Supplier<Integer> defaultBookModeReachabilitySupplier,
+            Supplier<Integer> defaultTabletopModeReachabilitySupplier,
+            File configFolder,
             PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
         mContext = systemUiContext.createDeviceProtectedStorageContext();
         mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
         mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+        mDefaultBookModeReachabilitySupplier =
+                defaultBookModeReachabilitySupplier;
+        mDefaultTabletopModeReachabilitySupplier =
+                defaultTabletopModeReachabilitySupplier;
         mCompletionCallback = completionCallback;
         final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
         mConfigurationFile = new AtomicFile(prefFiles);
@@ -117,8 +140,12 @@
      * enabled.
      */
     @LetterboxHorizontalReachabilityPosition
-    int getLetterboxPositionForHorizontalReachability() {
-        return mLetterboxPositionForHorizontalReachability;
+    int getLetterboxPositionForHorizontalReachability(boolean forBookMode) {
+        if (forBookMode) {
+            return mLetterboxPositionForBookModeReachability;
+        } else {
+            return mLetterboxPositionForHorizontalReachability;
+        }
     }
 
     /*
@@ -126,31 +153,55 @@
      * enabled.
      */
     @LetterboxVerticalReachabilityPosition
-    int getLetterboxPositionForVerticalReachability() {
-        return mLetterboxPositionForVerticalReachability;
-    }
-
-    /**
-     * Updates letterboxPositionForVerticalReachability if different from the current value
-     */
-    void setLetterboxPositionForHorizontalReachability(
-            int letterboxPositionForHorizontalReachability) {
-        if (mLetterboxPositionForHorizontalReachability
-                != letterboxPositionForHorizontalReachability) {
-            mLetterboxPositionForHorizontalReachability =
-                    letterboxPositionForHorizontalReachability;
-            updateConfiguration();
+    int getLetterboxPositionForVerticalReachability(boolean forTabletopMode) {
+        if (forTabletopMode) {
+            return mLetterboxPositionForTabletopModeReachability;
+        } else {
+            return mLetterboxPositionForVerticalReachability;
         }
     }
 
     /**
      * Updates letterboxPositionForVerticalReachability if different from the current value
      */
-    void setLetterboxPositionForVerticalReachability(
+    void setLetterboxPositionForHorizontalReachability(boolean forBookMode,
+            int letterboxPositionForHorizontalReachability) {
+        if (forBookMode) {
+            if (mLetterboxPositionForBookModeReachability
+                    != letterboxPositionForHorizontalReachability) {
+                mLetterboxPositionForBookModeReachability =
+                        letterboxPositionForHorizontalReachability;
+                updateConfiguration();
+            }
+        } else {
+            if (mLetterboxPositionForHorizontalReachability
+                    != letterboxPositionForHorizontalReachability) {
+                mLetterboxPositionForHorizontalReachability =
+                        letterboxPositionForHorizontalReachability;
+                updateConfiguration();
+            }
+        }
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForVerticalReachability(boolean forTabletopMode,
             int letterboxPositionForVerticalReachability) {
-        if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
-            mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
-            updateConfiguration();
+        if (forTabletopMode) {
+            if (mLetterboxPositionForTabletopModeReachability
+                    != letterboxPositionForVerticalReachability) {
+                mLetterboxPositionForTabletopModeReachability =
+                        letterboxPositionForVerticalReachability;
+                updateConfiguration();
+            }
+        } else {
+            if (mLetterboxPositionForVerticalReachability
+                    != letterboxPositionForVerticalReachability) {
+                mLetterboxPositionForVerticalReachability =
+                        letterboxPositionForVerticalReachability;
+                updateConfiguration();
+            }
         }
     }
 
@@ -158,6 +209,10 @@
     void useDefaultValue() {
         mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
         mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+        mLetterboxPositionForBookModeReachability =
+                mDefaultBookModeReachabilitySupplier.get();
+        mLetterboxPositionForTabletopModeReachability =
+                mDefaultTabletopModeReachabilitySupplier.get();
     }
 
     private void readCurrentConfiguration() {
@@ -171,6 +226,10 @@
                     letterboxData.letterboxPositionForHorizontalReachability;
             mLetterboxPositionForVerticalReachability =
                     letterboxData.letterboxPositionForVerticalReachability;
+            mLetterboxPositionForBookModeReachability =
+                    letterboxData.letterboxPositionForBookModeReachability;
+            mLetterboxPositionForTabletopModeReachability =
+                    letterboxData.letterboxPositionForTabletopModeReachability;
         } catch (IOException ioe) {
             Slog.e(TAG,
                     "Error reading from LetterboxConfigurationPersister. "
@@ -192,6 +251,8 @@
         mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
                 mLetterboxPositionForHorizontalReachability,
                 mLetterboxPositionForVerticalReachability,
+                mLetterboxPositionForBookModeReachability,
+                mLetterboxPositionForTabletopModeReachability,
                 mCompletionCallback), /* flush */ true);
     }
 
@@ -221,13 +282,18 @@
 
         private final int mHorizontalReachability;
         private final int mVerticalReachability;
+        private final int mBookModeReachability;
+        private final int mTabletopModeReachability;
 
         UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
                 int horizontalReachability, int verticalReachability,
+                int bookModeReachability, int tabletopModeReachability,
                 @Nullable Consumer<String> onComplete) {
             mFileToUpdate = fileToUpdate;
             mHorizontalReachability = horizontalReachability;
             mVerticalReachability = verticalReachability;
+            mBookModeReachability = bookModeReachability;
+            mTabletopModeReachability = tabletopModeReachability;
             mOnComplete = onComplete;
         }
 
@@ -237,6 +303,10 @@
                     new WindowManagerProtos.LetterboxProto();
             letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
             letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+            letterboxData.letterboxPositionForBookModeReachability =
+                    mBookModeReachability;
+            letterboxData.letterboxPositionForTabletopModeReachability =
+                    mTabletopModeReachability;
             final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
 
             FileOutputStream fos = null;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index a53a5fc..9cb94c6 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -275,30 +275,62 @@
                 && mActivityRecord.fillsParent();
     }
 
+    // Check if we are in the given pose and in fullscreen mode.
+    // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+    // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+    // actually fullscreen.
+    private boolean isDisplayFullScreenAndInPosture(DeviceStateController.FoldState state,
+            boolean isTabletop) {
+        Task task = mActivityRecord.getTask();
+        return mActivityRecord.mDisplayContent != null
+                && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
+                    isTabletop)
+                && task != null
+                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    }
+
+    // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+    // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+    // actually fullscreen.
+    private boolean isDisplayFullScreenAndSeparatingHinge() {
+        Task task = mActivityRecord.getTask();
+        return mActivityRecord.mDisplayContent != null
+                && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
+                && task != null
+                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    }
+
+
     float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
         // Don't check resolved configuration because it may not be updated yet during
         // configuration change.
+        boolean bookMode = isDisplayFullScreenAndInPosture(
+                DeviceStateController.FoldState.HALF_FOLDED, false /* isTabletop */);
         return isHorizontalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
-                ? mLetterboxConfiguration.getHorizontalMultiplierForReachability()
-                : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier();
+                ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookMode)
+                : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookMode);
     }
 
     float getVerticalPositionMultiplier(Configuration parentConfiguration) {
         // Don't check resolved configuration because it may not be updated yet during
         // configuration change.
+        boolean tabletopMode = isDisplayFullScreenAndInPosture(
+                DeviceStateController.FoldState.HALF_FOLDED, true /* isTabletop */);
         return isVerticalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
-                ? mLetterboxConfiguration.getVerticalMultiplierForReachability()
-                : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier();
+                ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
+                : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
     }
 
     float getFixedOrientationLetterboxAspectRatio() {
-        return mActivityRecord.shouldCreateCompatDisplayInsets()
-                ? getDefaultMinAspectRatioForUnresizableApps()
-                : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+        return isDisplayFullScreenAndSeparatingHinge()
+                ? getSplitScreenAspectRatio()
+                : mActivityRecord.shouldCreateCompatDisplayInsets()
+                    ? getDefaultMinAspectRatioForUnresizableApps()
+                    : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
     }
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -351,11 +383,13 @@
             return;
         }
 
+        boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge();
         int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
-                .getLetterboxPositionForHorizontalReachability();
+                .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
         if (mLetterbox.getInnerFrame().left > x) {
             // Moving to the next stop on the left side of the app window: right > center > left.
-            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop();
+            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
+                    isInFullScreenBookMode);
             int changeToLog =
                     letterboxPositionForHorizontalReachability
                             == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -364,7 +398,8 @@
             logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().right < x) {
             // Moving to the next stop on the right side of the app window: left > center > right.
-            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+                    isInFullScreenBookMode);
             int changeToLog =
                     letterboxPositionForHorizontalReachability
                             == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -388,11 +423,13 @@
             // Only react to clicks at the top and bottom of the letterboxed app window.
             return;
         }
+        boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
         int letterboxPositionForVerticalReachability = mLetterboxConfiguration
-                .getLetterboxPositionForVerticalReachability();
+                .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
         if (mLetterbox.getInnerFrame().top > y) {
             // Moving to the next stop on the top side of the app window: bottom > center > top.
-            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop();
+            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
+                    isInFullScreenTabletopMode);
             int changeToLog =
                     letterboxPositionForVerticalReachability
                             == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -401,7 +438,8 @@
             logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().bottom < y) {
             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
-            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+                    isInFullScreenTabletopMode);
             int changeToLog =
                     letterboxPositionForVerticalReachability
                             == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -712,10 +750,10 @@
                 + getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
         pw.println(prefix + "  letterboxPositionForHorizontalReachability="
                 + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
-                    mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+                mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
         pw.println(prefix + "  letterboxPositionForVerticalReachability="
                 + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
-                    mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+                mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
         pw.println(prefix + "  fixedOrientationLetterboxAspectRatio="
                 + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
         pw.println(prefix + "  defaultMinAspectRatioForUnresizableApps="
@@ -780,14 +818,20 @@
         int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
         if (isHorizontalReachabilityEnabled()) {
             int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
-                    .getLetterboxPositionForHorizontalReachability();
+                    .getLetterboxPositionForHorizontalReachability(
+                            isDisplayFullScreenAndInPosture(
+                                    DeviceStateController.FoldState.HALF_FOLDED,
+                                    false /* isTabletop */));
             positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
-                            letterboxPositionForHorizontalReachability);
+                    letterboxPositionForHorizontalReachability);
         } else if (isVerticalReachabilityEnabled()) {
             int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
-                    .getLetterboxPositionForVerticalReachability();
+                    .getLetterboxPositionForVerticalReachability(
+                            isDisplayFullScreenAndInPosture(
+                                    DeviceStateController.FoldState.HALF_FOLDED,
+                                    true /* isTabletop */));
             positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
-                            letterboxPositionForVerticalReachability);
+                    letterboxPositionForVerticalReachability);
         }
         return positionToLog;
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 4e692e2d..85aa942 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1219,9 +1219,17 @@
             pw.println("Corner radius: "
                     + mLetterboxConfiguration.getLetterboxActivityCornersRadius());
             pw.println("Horizontal position multiplier: "
-                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
+                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+                            false /* isInBookMode */));
             pw.println("Vertical position multiplier: "
-                    + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier());
+                    + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+                            false /* isInTabletopMode */));
+            pw.println("Horizontal position multiplier (book mode): "
+                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+                            true /* isInBookMode */));
+            pw.println("Vertical position multiplier (tabletop mode): "
+                    + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+                            true /* isInTabletopMode */));
             pw.println("Aspect ratio: "
                     + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
             pw.println("Default min aspect ratio for unresizable apps: "
@@ -1238,10 +1246,10 @@
                     mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
             pw.println("Current position for horizontal reachability:"
                     + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
-                        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+                    mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
             pw.println("Current position for vertical reachability:"
                     + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
-                        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+                    mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
             pw.println("Is education enabled: "
                     + mLetterboxConfiguration.getIsEducationEnabled());
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
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();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
index 1be9de7..51a7e74 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -67,6 +67,11 @@
                         R.integer.config_letterboxDefaultPositionForHorizontalReachability),
                 () -> mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForVerticalReachability),
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForBookModeReachability),
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+                ),
                 mConfigFolder, mPersisterQueue, mQueueState);
         mQueueListener = queueEmpty -> mQueueState.onItemAdded();
         mPersisterQueue.addListener(mQueueListener);
@@ -84,14 +89,15 @@
     @Test
     public void test_whenStoreIsCreated_valuesAreDefaults() {
         final int positionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        false);
         final int defaultPositionForHorizontalReachability =
                 mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForHorizontalReachability);
         Assert.assertEquals(defaultPositionForHorizontalReachability,
                 positionForHorizontalReachability);
         final int positionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         final int defaultPositionForVerticalReachability =
                 mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForVerticalReachability);
@@ -101,15 +107,16 @@
 
     @Test
     public void test_whenUpdatedWithNewValues_valuesAreWritten() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
         waitForCompletion(mPersisterQueue);
         final int newPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        false);
         final int newPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 newPositionForHorizontalReachability);
         Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -120,24 +127,24 @@
     public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
         final PersisterQueue firstPersisterQueue = new PersisterQueue();
         final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
-                mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
-                mQueueState);
+                mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+                firstPersisterQueue, mQueueState);
         firstPersister.start();
-        firstPersister.setLetterboxPositionForHorizontalReachability(
+        firstPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        firstPersister.setLetterboxPositionForVerticalReachability(
+        firstPersister.setLetterboxPositionForVerticalReachability(false,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
         waitForCompletion(firstPersisterQueue);
         stopPersisterSafe(firstPersisterQueue);
         final PersisterQueue secondPersisterQueue = new PersisterQueue();
         final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
-                mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
-                mQueueState);
+                mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+                secondPersisterQueue, mQueueState);
         secondPersister.start();
         final int newPositionForHorizontalReachability =
-                secondPersister.getLetterboxPositionForHorizontalReachability();
+                secondPersister.getLetterboxPositionForHorizontalReachability(false);
         final int newPositionForVerticalReachability =
-                secondPersister.getLetterboxPositionForVerticalReachability();
+                secondPersister.getLetterboxPositionForVerticalReachability(false);
         Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 newPositionForHorizontalReachability);
         Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -149,15 +156,16 @@
 
     @Test
     public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
         waitForCompletion(mPersisterQueue);
         final int newPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        false);
         final int newPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 newPositionForHorizontalReachability);
         Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -165,14 +173,15 @@
         deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
         waitForCompletion(mPersisterQueue);
         final int positionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                        false);
         final int defaultPositionForHorizontalReachability =
                 mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForHorizontalReachability);
         Assert.assertEquals(defaultPositionForHorizontalReachability,
                 positionForHorizontalReachability);
         final int positionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         final int defaultPositionForVerticalReachability =
                 mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForVerticalReachability);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index c927f9e..e196704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -26,6 +26,7 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -39,7 +40,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.function.Consumer;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
 
 @SmallTest
 @Presubmit
@@ -58,20 +60,28 @@
 
     @Test
     public void test_whenReadingValues_storeIsInvoked() {
-        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
-        verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
-        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
-        verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+        for (boolean halfFoldPose : Arrays.asList(false, true)) {
+            mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose);
+            verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(
+                    halfFoldPose);
+            mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose);
+            verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(
+                    halfFoldPose);
+        }
     }
 
     @Test
     public void test_whenSettingValues_updateConfigurationIsInvoked() {
-        mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
-                anyInt());
-        mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
-                anyInt());
+        for (boolean halfFoldPose : Arrays.asList(false, true)) {
+            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+                    halfFoldPose);
+            verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+                    eq(halfFoldPose), anyInt());
+            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+                    halfFoldPose);
+            verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+                    eq(halfFoldPose), anyInt());
+        }
     }
 
     @Test
@@ -81,33 +91,65 @@
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         // Starting from left
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         // Starting from right
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        // Starting from left - book mode
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 1,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 1,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from right - book mode
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 2,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 2,
+                /* halfFoldPose */ true,
                 LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
     }
 
@@ -118,55 +160,87 @@
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         // Starting from top
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 1,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         // Starting from bottom
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 2,
+                /* halfFoldPose */ false,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        // Starting from top - tabletop mode
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 1,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 1,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from bottom - tabletop mode
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 2,
+                /* halfFoldPose */ true,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 2,
+                /* halfFoldPose */ true,
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
     }
 
     private void assertForHorizontalMove(int from, int expected, int expectedTime,
-            Consumer<LetterboxConfiguration> move) {
+            boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
         // We are in the current position
-        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose))
                 .thenReturn(from);
-        move.accept(mLetterboxConfiguration);
+        move.accept(mLetterboxConfiguration, halfFoldPose);
         verify(mLetterboxConfigurationPersister,
-                times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+                times(expectedTime)).setLetterboxPositionForHorizontalReachability(halfFoldPose,
                 expected);
     }
 
     private void assertForVerticalMove(int from, int expected, int expectedTime,
-            Consumer<LetterboxConfiguration> move) {
+            boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
         // We are in the current position
-        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose))
                 .thenReturn(from);
-        move.accept(mLetterboxConfiguration);
+        move.accept(mLetterboxConfiguration, halfFoldPose);
         verify(mLetterboxConfigurationPersister,
-                times(expectedTime)).setLetterboxPositionForVerticalReachability(
+                times(expectedTime)).setLetterboxPositionForVerticalReachability(halfFoldPose,
                 expected);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 94c33f2..7488e1c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
@@ -95,6 +96,7 @@
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.wm.DeviceStateController.FoldState;
 
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -2821,6 +2823,70 @@
     }
 
     @Test
+    public void testUpdateResolvedBoundsVerticalPosition_tabletop() {
+
+        // Set up a display in portrait with a fixed-orientation LANDSCAPE app
+        setUpDisplaySizeWithApp(1400, 2800);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+                1.0f /*letterboxVerticalPositionMultiplier*/);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        Rect letterboxNoFold = new Rect(0, 2100, 1400, 2800);
+        assertEquals(letterboxNoFold, mActivity.getBounds());
+
+        // Make the activity full-screen
+        mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+        setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+        Rect letterboxHalfFold = new Rect(0, 0, 1400, 700);
+        assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+        setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+        assertEquals(letterboxNoFold, mActivity.getBounds());
+
+    }
+
+    @Test
+    public void testUpdateResolvedBoundsHorizontalPosition_book() {
+
+        // Set up a display in landscape with a fixed-orientation PORTRAIT app
+        setUpDisplaySizeWithApp(2800, 1400);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+                1.0f /*letterboxVerticalPositionMultiplier*/);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+        Rect letterboxNoFold = new Rect(2100, 0, 2800, 1400);
+        assertEquals(letterboxNoFold, mActivity.getBounds());
+
+        // Make the activity full-screen
+        mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+        setFoldablePosture(true /* isHalfFolded */, false /* isTabletop */);
+
+        Rect letterboxHalfFold = new Rect(0, 0, 700, 1400);
+        assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+        setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+        assertEquals(letterboxNoFold, mActivity.getBounds());
+
+    }
+
+    private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
+        final DisplayRotation r = mActivity.mDisplayContent.getDisplayRotation();
+        doReturn(isHalfFolded).when(r).isDisplaySeparatingHinge();
+        doReturn(false).when(r).isDeviceInPosture(any(FoldState.class), anyBoolean());
+        if (isHalfFolded) {
+            doReturn(true).when(r).isDeviceInPosture(FoldState.HALF_FOLDED, isTabletop);
+        }
+        mActivity.recomputeConfiguration();
+    }
+
+    @Test
     public void testUpdateResolvedBoundsPosition_alignToTop() {
         final int notchHeight = 100;
         final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)