Merge "[Output Switcher] Update stop button text" into tm-qpr-dev
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b26504d..5985d6e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -29,6 +29,8 @@
 import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
 import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS;
 import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
+import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
+import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
@@ -194,6 +196,7 @@
 import android.window.SplashScreenView;
 import android.window.WindowProviderService;
 
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractor;
@@ -5890,9 +5893,21 @@
         final boolean shouldUpdateResources = hasPublicResConfigChange
                 || shouldUpdateResources(activityToken, currentResConfig, newConfig,
                 amOverrideConfig, movedToDifferentDisplay, hasPublicResConfigChange);
+
+        // TODO(b/266924897): temporary workaround, remove for U.
+        boolean skipActivityRelaunchWhenDocking = activity.getResources().getBoolean(
+                R.bool.config_skipActivityRelaunchWhenDocking);
+        int handledConfigChanges = activity.mActivityInfo.getRealConfigChanged();
+        if (skipActivityRelaunchWhenDocking && onlyDeskInUiModeChanged(activity.mCurrentConfig,
+                newConfig)) {
+            // If we're not relaunching this activity when docking, we should send the configuration
+            // changed event. Pretend as if the activity is handling uiMode config changes in its
+            // manifest so that we'll report any dock changes.
+            handledConfigChanges |= ActivityInfo.CONFIG_UI_MODE;
+        }
+
         final boolean shouldReportChange = shouldReportChange(activity.mCurrentConfig, newConfig,
-                r != null ? r.mSizeConfigurations : null,
-                activity.mActivityInfo.getRealConfigChanged());
+                r != null ? r.mSizeConfigurations : null, handledConfigChanges);
         // Nothing significant, don't proceed with updating and reporting.
         if (!shouldUpdateResources && !shouldReportChange) {
             return null;
@@ -5939,6 +5954,25 @@
     }
 
     /**
+     * Returns true if the uiMode configuration changed, and desk mode
+     * ({@link android.content.res.Configuration#UI_MODE_TYPE_DESK}) was the only change to uiMode.
+     */
+    private boolean onlyDeskInUiModeChanged(Configuration oldConfig, Configuration newConfig) {
+        boolean deskModeChanged = isInDeskUiMode(oldConfig) != isInDeskUiMode(newConfig);
+
+        // UI mode contains fields other than the UI mode type, so determine if any other fields
+        // changed.
+        boolean uiModeOtherFieldsChanged =
+                (oldConfig.uiMode & ~UI_MODE_TYPE_MASK) != (newConfig.uiMode & ~UI_MODE_TYPE_MASK);
+
+        return deskModeChanged && !uiModeOtherFieldsChanged;
+    }
+
+    private static boolean isInDeskUiMode(Configuration config) {
+        return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_DESK;
+    }
+
+    /**
      * Returns {@code true} if {@link Activity#onConfigurationChanged(Configuration)} should be
      * dispatched.
      *
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 23f2bdb..1551ce9 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -242,6 +242,12 @@
     public boolean isLetterboxDoubleTapEnabled;
 
     /**
+     * Whether the update comes from a letterbox double-tap action from the user or not.
+     * @hide
+     */
+    public boolean isFromLetterboxDoubleTap;
+
+    /**
      * If {@link isLetterboxDoubleTapEnabled} it contains the current letterbox vertical position or
      * {@link TaskInfo.PROPERTY_VALUE_UNSET} otherwise.
      * @hide
@@ -488,7 +494,7 @@
                 && isResizeable == that.isResizeable
                 && supportsMultiWindow == that.supportsMultiWindow
                 && displayAreaFeatureId == that.displayAreaFeatureId
-                && isLetterboxDoubleTapEnabled == that.isLetterboxDoubleTapEnabled
+                && isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap
                 && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition
                 && topActivityLetterboxWidth == that.topActivityLetterboxWidth
                 && topActivityLetterboxHeight == that.topActivityLetterboxHeight
@@ -520,9 +526,9 @@
         return displayId == that.displayId
                 && taskId == that.taskId
                 && topActivityInSizeCompat == that.topActivityInSizeCompat
+                && isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap
                 && topActivityEligibleForLetterboxEducation
                     == that.topActivityEligibleForLetterboxEducation
-                && isLetterboxDoubleTapEnabled == that.isLetterboxDoubleTapEnabled
                 && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition
                 && topActivityLetterboxHorizontalPosition
                     == that.topActivityLetterboxHorizontalPosition
@@ -583,6 +589,7 @@
         displayAreaFeatureId = source.readInt();
         cameraCompatControlState = source.readInt();
         isLetterboxDoubleTapEnabled = source.readBoolean();
+        isFromLetterboxDoubleTap = source.readBoolean();
         topActivityLetterboxVerticalPosition = source.readInt();
         topActivityLetterboxHorizontalPosition = source.readInt();
         topActivityLetterboxWidth = source.readInt();
@@ -635,6 +642,7 @@
         dest.writeInt(displayAreaFeatureId);
         dest.writeInt(cameraCompatControlState);
         dest.writeBoolean(isLetterboxDoubleTapEnabled);
+        dest.writeBoolean(isFromLetterboxDoubleTap);
         dest.writeInt(topActivityLetterboxVerticalPosition);
         dest.writeInt(topActivityLetterboxHorizontalPosition);
         dest.writeInt(topActivityLetterboxWidth);
@@ -675,6 +683,7 @@
                 + " topActivityEligibleForLetterboxEducation= "
                         + topActivityEligibleForLetterboxEducation
                 + " topActivityLetterboxed= " + isLetterboxDoubleTapEnabled
+                + " isFromDoubleTap= " + isFromLetterboxDoubleTap
                 + " topActivityLetterboxVerticalPosition= " + topActivityLetterboxVerticalPosition
                 + " topActivityLetterboxHorizontalPosition= "
                         + topActivityLetterboxHorizontalPosition
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index c8bbb0c1..6f09e79 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -1453,6 +1453,16 @@
     }
 
     /**
+     * @hide
+     */
+    Configuration[] getSizeAndUiModeConfigurations() {
+        synchronized (this) {
+            ensureValidLocked();
+            return nativeGetSizeAndUiModeConfigurations(mObject);
+        }
+    }
+
+    /**
      * Change the configuration used when retrieving resources.  Not for use by
      * applications.
      * @hide
@@ -1603,6 +1613,7 @@
     private static native @Nullable String nativeGetResourceEntryName(long ptr, @AnyRes int resid);
     private static native @Nullable String[] nativeGetLocales(long ptr, boolean excludeSystem);
     private static native @Nullable Configuration[] nativeGetSizeConfigurations(long ptr);
+    private static native @Nullable Configuration[] nativeGetSizeAndUiModeConfigurations(long ptr);
     private static native void nativeSetResourceResolutionLoggingEnabled(long ptr, boolean enabled);
     private static native @Nullable String nativeGetLastResourceResolution(long ptr);
 
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index a03286d3..9b16949 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -2207,6 +2207,11 @@
         return mResourcesImpl.getSizeConfigurations();
     }
 
+    /** @hide */
+    public Configuration[] getSizeAndUiModeConfigurations() {
+        return mResourcesImpl.getSizeAndUiModeConfigurations();
+    }
+
     /**
      * Return the compatibility mode information for the application.
      * The returned object should be treated as read-only.
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index ff07291..3bb237a 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -203,6 +203,10 @@
         return mAssets.getSizeConfigurations();
     }
 
+    Configuration[] getSizeAndUiModeConfigurations() {
+        return mAssets.getSizeAndUiModeConfigurations();
+    }
+
     CompatibilityInfo getCompatibilityInfo() {
         return mDisplayAdjustments.getCompatibilityInfo();
     }
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 8c23b21..206ad17 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -93,6 +93,7 @@
   jfieldID mScreenWidthDpOffset;
   jfieldID mScreenHeightDpOffset;
   jfieldID mScreenLayoutOffset;
+  jfieldID mUiMode;
 } gConfigurationOffsets;
 
 static struct arraymap_offsets_t {
@@ -1027,10 +1028,11 @@
   env->SetIntField(result, gConfigurationOffsets.mScreenWidthDpOffset, config.screenWidthDp);
   env->SetIntField(result, gConfigurationOffsets.mScreenHeightDpOffset, config.screenHeightDp);
   env->SetIntField(result, gConfigurationOffsets.mScreenLayoutOffset, config.screenLayout);
+  env->SetIntField(result, gConfigurationOffsets.mUiMode, config.uiMode);
   return result;
 }
 
-static jobjectArray NativeGetSizeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
+static jobjectArray GetSizeAndUiModeConfigurations(JNIEnv* env, jlong ptr) {
   ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
   auto configurations = assetmanager->GetResourceConfigurations(true /*exclude_system*/,
                                                                 false /*exclude_mipmap*/);
@@ -1057,6 +1059,14 @@
   return array;
 }
 
+static jobjectArray NativeGetSizeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
+  return GetSizeAndUiModeConfigurations(env, ptr);
+}
+
+static jobjectArray NativeGetSizeAndUiModeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
+  return GetSizeAndUiModeConfigurations(env, ptr);
+}
+
 static jintArray NativeAttributeResolutionStack(
     JNIEnv* env, jclass /*clazz*/, jlong ptr,
     jlong theme_ptr, jint xml_style_res,
@@ -1487,6 +1497,8 @@
     {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales},
     {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;",
      (void*)NativeGetSizeConfigurations},
+    {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;",
+     (void*)NativeGetSizeAndUiModeConfigurations},
 
     // Style attribute related methods.
     {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack},
@@ -1565,6 +1577,7 @@
       GetFieldIDOrDie(env, configurationClass, "screenHeightDp", "I");
   gConfigurationOffsets.mScreenLayoutOffset =
           GetFieldIDOrDie(env, configurationClass, "screenLayout", "I");
+  gConfigurationOffsets.mUiMode = GetFieldIDOrDie(env, configurationClass, "uiMode", "I");
 
   jclass arrayMapClass = FindClassOrDie(env, "android/util/ArrayMap");
   gArrayMapOffsets.classObject = MakeGlobalRefOrDie(env, arrayMapClass);
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 46aaa1f..92d2332 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5422,10 +5422,20 @@
         split screen. -->
     <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool>
 
+    <!-- Whether should use split screen aspect ratio for the activity when camera compat treatment
+        is enabled and activity is connected to the camera in fullscreen. -->
+    <bool name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled">false</bool>
+
     <!-- Whether a camera compat controller is enabled to allow the user to apply or revert
          treatment for stretched issues in camera viewfinder. -->
     <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
 
+    <!-- Docking is a uiMode configuration change and will cause activities to relaunch if it's not
+         handled. If true, the configuration change will be sent but activities will not be
+         relaunched upon docking. Apps with desk resources will behave like normal, since they may
+         expect the relaunch upon the desk uiMode change. -->
+    <bool name="config_skipActivityRelaunchWhenDocking">false</bool>
+
     <!-- If true, hide the display cutout with display area -->
     <bool name="config_hideDisplayCutoutWithDisplayArea">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1a3feb8..d8b261c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4489,7 +4489,9 @@
   <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
   <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
+  <java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" />
   <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
+  <java-symbol type="bool" name="config_skipActivityRelaunchWhenDocking" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index 902c41c..4e10ce8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -156,15 +156,13 @@
 
     void setDontShowReachabilityEducationAgain(TaskInfo taskInfo) {
         mCompatUISharedPreferences.edit().putBoolean(
-                getDontShowAgainReachabilityEduKey(taskInfo.userId,
-                        taskInfo.topActivity.getPackageName()), true).apply();
+                getDontShowAgainReachabilityEduKey(taskInfo.userId), true).apply();
     }
 
     boolean shouldShowReachabilityEducation(@NonNull TaskInfo taskInfo) {
         return getHasSeenLetterboxEducation(taskInfo.userId)
                 && !mCompatUISharedPreferences.getBoolean(
-                getDontShowAgainReachabilityEduKey(taskInfo.userId,
-                        taskInfo.topActivity.getPackageName()), /* default= */false);
+                getDontShowAgainReachabilityEduKey(taskInfo.userId), /* default= */false);
     }
 
     boolean getHasSeenLetterboxEducation(int userId) {
@@ -206,8 +204,8 @@
         }
     }
 
-    private static String getDontShowAgainReachabilityEduKey(int userId, String packageName) {
-        return HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX + "_" + packageName + "@" + userId;
+    private static String getDontShowAgainReachabilityEduKey(int userId) {
+        return HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX + "@" + userId;
     }
 
     private static String getDontShowLetterboxEduKey(int userId) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
index d44b4d8..f65c26a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
@@ -21,6 +21,7 @@
 import android.app.TaskInfo.CameraCompatControlState;
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
@@ -112,6 +113,14 @@
     }
 
     @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mWindowManager.relayout();
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
index 6223efa..f1b098e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
@@ -74,9 +74,7 @@
     private boolean mForceUpdate = false;
 
     // We decided to force the visualization of the double-tap animated icons every time the user
-    // double-taps. We detect a double-tap checking the previous and current state of
-    // mLetterboxVerticalPosition and mLetterboxHorizontalPosition saving the result in this
-    // variable.
+    // double-taps.
     private boolean mHasUserDoubleTapped;
 
     // When the size of the letterboxed app changes and the icons are visible
@@ -155,11 +153,9 @@
         mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition;
         mTopActivityLetterboxWidth = taskInfo.topActivityLetterboxWidth;
         mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight;
+        mHasUserDoubleTapped = taskInfo.isFromLetterboxDoubleTap;
 
-        mHasUserDoubleTapped =
-                mLetterboxVerticalPosition != prevLetterboxVerticalPosition
-                        || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition;
-        if (mHasUserDoubleTapped) {
+        if (taskInfo.isFromLetterboxDoubleTap) {
             // In this case we disable the reachability for the following launch of
             // the current application. Anyway because a double tap event happened,
             // the reachability education is displayed
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 1187126..4c53f60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -210,7 +210,7 @@
     /**
      * Quietly cancel the animator by removing the listeners first.
      */
-    public static void quietCancel(@NonNull ValueAnimator animator) {
+    static void quietCancel(@NonNull ValueAnimator animator) {
         animator.removeAllUpdateListeners();
         animator.removeAllListeners();
         animator.cancel();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 1dd2ef9..7e9f2c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -147,10 +147,12 @@
     // These callbacks are called on the update thread
     private final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
             new PipAnimationController.PipAnimationCallback() {
+        private boolean mIsCancelled;
         @Override
         public void onPipAnimationStart(TaskInfo taskInfo,
                 PipAnimationController.PipTransitionAnimator animator) {
             final int direction = animator.getTransitionDirection();
+            mIsCancelled = false;
             sendOnPipTransitionStarted(direction);
         }
 
@@ -158,6 +160,10 @@
         public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx,
                 PipAnimationController.PipTransitionAnimator animator) {
             final int direction = animator.getTransitionDirection();
+            if (mIsCancelled) {
+                sendOnPipTransitionFinished(direction);
+                return;
+            }
             final int animationType = animator.getAnimationType();
             final Rect destinationBounds = animator.getDestinationBounds();
             if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
@@ -196,6 +202,7 @@
         public void onPipAnimationCancel(TaskInfo taskInfo,
                 PipAnimationController.PipTransitionAnimator animator) {
             final int direction = animator.getTransitionDirection();
+            mIsCancelled = true;
             if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
                 fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(),
                         animator::clearContentOverlay, true /* withStartDelay */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 9807320..2bd5f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -779,7 +779,7 @@
                     mPipAnimationController.getCurrentAnimator();
             if (animator != null && animator.isRunning()) {
                 // cancel any running animator, as it is using stale display layout information
-                PipAnimationController.quietCancel(animator);
+                animator.cancel();
             }
             onDisplayChangedUncheck(layout, saveRestoreSnapFraction);
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
index 3f79df6..12ceb0a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
@@ -66,6 +66,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.HashSet;
+import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -118,6 +120,18 @@
         mExecutor = new TestShellExecutor();
         mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) {
 
+            final Set<Integer> mHasSeenSet = new HashSet<>();
+
+            @Override
+            boolean getHasSeenLetterboxEducation(int userId) {
+                return mHasSeenSet.contains(userId);
+            }
+
+            @Override
+            void setSeenLetterboxEducation(int userId) {
+                mHasSeenSet.add(userId);
+            }
+
             @Override
             protected String getCompatUISharedPreferenceName() {
                 return TEST_COMPAT_UI_SHARED_PREFERENCES;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index 91e1e19..359ef97 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
-import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
 import android.app.TaskInfo;
@@ -32,7 +31,6 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -71,10 +69,6 @@
         mExecutor = new TestShellExecutor();
     }
 
-    @After
-    public void tearDown() {
-    }
-
     @Test
     public void testCreateLayout_notEligible_doesNotCreateLayout() {
         final ReachabilityEduWindowManager windowManager = createReachabilityEduWindowManager(
@@ -85,20 +79,6 @@
         assertNull(windowManager.mLayout);
     }
 
-    @Test
-    public void testCreateLayout_letterboxPositionChanged_doubleTapIsDetected() {
-        // Initial left position
-        final TaskInfo initialTaskInfo = createTaskInfoForHorizontalTapping(USER_ID, 0, 1000);
-        final ReachabilityEduWindowManager windowManager =
-                createReachabilityEduWindowManager(initialTaskInfo);
-        // Move to the right
-        final TaskInfo newPositionTaskInfo = createTaskInfoForHorizontalTapping(USER_ID, 1, 1000);
-        windowManager.updateCompatInfo(newPositionTaskInfo, mTaskListener, /* canShow */ true);
-
-        verify(mCompatUIConfiguration).setDontShowReachabilityEducationAgain(newPositionTaskInfo);
-    }
-
-
     private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) {
         return new ReachabilityEduWindowManager(mContext, taskInfo,
                 mSyncTransactionQueue, mCallback, mTaskListener, mDisplayLayout,
@@ -113,14 +93,6 @@
                 /* topActivityLetterboxHeight */ -1);
     }
 
-    private static TaskInfo createTaskInfoForHorizontalTapping(int userId,
-            int topActivityLetterboxHorizontalPosition, int topActivityLetterboxWidth) {
-        return createTaskInfo(userId, /* isLetterboxDoubleTapEnabled */ true,
-                /* topActivityLetterboxVerticalPosition */ -1,
-                topActivityLetterboxHorizontalPosition, topActivityLetterboxWidth,
-                /* topActivityLetterboxHeight */ -1);
-    }
-
     private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled,
             int topActivityLetterboxVerticalPosition, int topActivityLetterboxHorizontalPosition,
             int topActivityLetterboxWidth, int topActivityLetterboxHeight) {
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index bf264f8f..031c3ff 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -107,7 +107,7 @@
     private final Map<OnMediaKeyEventSessionChangedListener, Executor>
             mMediaKeyEventSessionChangedCallbacks = new HashMap<>();
     @GuardedBy("mLock")
-    private String mCurMediaKeyEventSessionPackage;
+    private String mCurMediaKeyEventSessionPackage = "";
     @GuardedBy("mLock")
     private MediaSession.Token mCurMediaKeyEventSession;
     @GuardedBy("mLock")
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index efcb6f3..8bff1a1 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -29,9 +29,10 @@
         android:format12Hour="@string/dream_time_complication_12_hr_time_format"
         android:format24Hour="@string/dream_time_complication_24_hr_time_format"
         android:fontFeatureSettings="pnum, lnum"
+        android:includeFontPadding="false"
         android:letterSpacing="0.02"
+        android:maxLines="1"
         android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"
-        android:translationY="@dimen/dream_overlay_complication_clock_time_translation_y"
         app:keyShadowBlur="@dimen/dream_overlay_clock_key_text_shadow_radius"
         app:keyShadowOffsetX="@dimen/dream_overlay_clock_key_text_shadow_dx"
         app:keyShadowOffsetY="@dimen/dream_overlay_clock_key_text_shadow_dy"
@@ -40,6 +41,7 @@
         app:ambientShadowOffsetX="@dimen/dream_overlay_clock_ambient_text_shadow_dx"
         app:ambientShadowOffsetY="@dimen/dream_overlay_clock_ambient_text_shadow_dy"
         app:ambientShadowAlpha="0.3"
-        />
+        app:removeTextDescent="true"
+        app:textDescentExtraPadding="@dimen/dream_overlay_clock_text_descent_extra_padding" />
 
 </FrameLayout>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 9145982..127d67e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1591,7 +1591,6 @@
     <dimen name="dream_overlay_bottom_affordance_radius">32dp</dimen>
     <dimen name="dream_overlay_bottom_affordance_padding">14dp</dimen>
     <dimen name="dream_overlay_complication_clock_time_text_size">86dp</dimen>
-    <dimen name="dream_overlay_complication_clock_time_translation_y">28dp</dimen>
     <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen>
     <dimen name="dream_overlay_complication_preview_text_size">36sp</dimen>
     <dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen>
@@ -1692,6 +1691,7 @@
     <dimen name="dream_overlay_clock_ambient_text_shadow_dx">0dp</dimen>
     <dimen name="dream_overlay_clock_ambient_text_shadow_dy">0dp</dimen>
     <dimen name="dream_overlay_clock_ambient_text_shadow_radius">1dp</dimen>
+    <dimen name="dream_overlay_clock_text_descent_extra_padding">1dp</dimen>
 
     <!-- Shadow for dream overlay status bar complications -->
     <dimen name="dream_overlay_status_bar_key_text_shadow_dx">0.5dp</dimen>
diff --git a/packages/SystemUI/shared/res/values/attrs.xml b/packages/SystemUI/shared/res/values/attrs.xml
index f3aeaef..84ea6b7 100644
--- a/packages/SystemUI/shared/res/values/attrs.xml
+++ b/packages/SystemUI/shared/res/values/attrs.xml
@@ -40,6 +40,9 @@
         <attr name="ambientShadowOffsetX" />
         <attr name="ambientShadowOffsetY" />
         <attr name="ambientShadowAlpha" />
+        <attr name="removeTextDescent" format="boolean" />
+        <!-- padding to add back when removing text descent so it ensures text is not clipped -->
+        <attr name="textDescentExtraPadding" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="DoubleShadowTextView">
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt
index f2db129..5a6f184 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.shared.R
 import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo
 import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows
+import kotlin.math.floor
 
 /** Extension of [TextClock] which draws two shadows on the text (ambient and key shadows) */
 class DoubleShadowTextClock
@@ -89,6 +90,21 @@
                     ambientShadowOffsetY.toFloat(),
                     ambientShadowAlpha
                 )
+            val removeTextDescent =
+                attributes.getBoolean(R.styleable.DoubleShadowTextClock_removeTextDescent, false)
+            val textDescentExtraPadding =
+                attributes.getDimensionPixelSize(
+                    R.styleable.DoubleShadowTextClock_textDescentExtraPadding,
+                    0
+                )
+            if (removeTextDescent) {
+                setPaddingRelative(
+                    0,
+                    0,
+                    0,
+                    textDescentExtraPadding - floor(paint.fontMetrics.descent.toDouble()).toInt()
+                )
+            }
         } finally {
             attributes.recycle()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index f83885b..d6c85fb 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.classifier;
 
+import static com.android.systemui.classifier.FalsingModule.IS_FOLDABLE_DEVICE;
+
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
@@ -30,6 +32,7 @@
 import java.util.List;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * Acts as a cache and utility class for FalsingClassifiers.
@@ -46,6 +49,7 @@
     private BatteryController mBatteryController;
     private final FoldStateListener mFoldStateListener;
     private final DockManager mDockManager;
+    private boolean mIsFoldableDevice;
     private final float mXdpi;
     private final float mYdpi;
     private final List<SessionListener> mSessionListeners = new ArrayList<>();
@@ -70,7 +74,8 @@
             DisplayMetrics displayMetrics,
             BatteryController batteryController,
             FoldStateListener foldStateListener,
-            DockManager dockManager) {
+            DockManager dockManager,
+            @Named(IS_FOLDABLE_DEVICE) boolean isFoldableDevice) {
         mXdpi = displayMetrics.xdpi;
         mYdpi = displayMetrics.ydpi;
         mWidthPixels = displayMetrics.widthPixels;
@@ -78,6 +83,7 @@
         mBatteryController = batteryController;
         mFoldStateListener = foldStateListener;
         mDockManager = dockManager;
+        mIsFoldableDevice = isFoldableDevice;
 
         FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
         FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels());
@@ -417,7 +423,7 @@
     }
 
     public boolean isUnfolded() {
-        return Boolean.FALSE.equals(mFoldStateListener.getFolded());
+        return mIsFoldableDevice && Boolean.FALSE.equals(mFoldStateListener.getFolded());
     }
 
     /** Implement to be alerted abotu the beginning and ending of falsing tracking. */
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
index 5302af9..c7f3b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
@@ -43,6 +43,7 @@
     String LONG_TAP_TOUCH_SLOP = "falsing_long_tap_slop";
     String DOUBLE_TAP_TOUCH_SLOP = "falsing_double_tap_touch_slop";
     String DOUBLE_TAP_TIMEOUT_MS = "falsing_double_tap_timeout_ms";
+    String IS_FOLDABLE_DEVICE = "falsing_foldable_device";
 
     /** */
     @Binds
@@ -89,4 +90,16 @@
     static float providesLongTapTouchSlop(ViewConfiguration viewConfiguration) {
         return viewConfiguration.getScaledTouchSlop() * 1.25f;
     }
+
+    /** */
+    @Provides
+    @Named(IS_FOLDABLE_DEVICE)
+    static boolean providesIsFoldableDevice(@Main Resources resources) {
+        try {
+            return resources.getIntArray(
+                    com.android.internal.R.array.config_foldedDeviceStates).length != 0;
+        } catch (Resources.NotFoundException e) {
+            return false;
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
index 461cacc..0218f45 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
@@ -17,10 +17,15 @@
 
 package com.android.systemui.controls.start
 
+import android.content.BroadcastReceiver
 import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
 import android.os.UserHandle
+import android.os.UserManager
 import androidx.annotation.WorkerThread
 import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
@@ -53,6 +58,8 @@
         private val userTracker: UserTracker,
         private val authorizedPanelsRepository: AuthorizedPanelsRepository,
         private val selectedComponentRepository: SelectedComponentRepository,
+        private val userManager: UserManager,
+        private val broadcastDispatcher: BroadcastDispatcher,
 ) : CoreStartable {
 
     // These two controllers can only be accessed after `start` method once we've checked if the
@@ -71,7 +78,9 @@
             }
         }
 
-    override fun start() {
+    override fun start() {}
+
+    override fun onBootCompleted() {
         if (!controlsComponent.isEnabled()) {
             // Controls is disabled, we don't need this anymore
             return
@@ -112,11 +121,30 @@
     }
 
     private fun bindToPanel() {
+        if (userManager.isUserUnlocked(userTracker.userId)) {
+            bindToPanelInternal()
+        } else {
+            broadcastDispatcher.registerReceiver(
+                    receiver = object : BroadcastReceiver() {
+                        override fun onReceive(context: Context?, intent: Intent?) {
+                            if (userManager.isUserUnlocked(userTracker.userId)) {
+                                bindToPanelInternal()
+                                broadcastDispatcher.unregisterReceiver(this)
+                            }
+                        }
+                    },
+                    filter = IntentFilter(Intent.ACTION_USER_UNLOCKED),
+                    executor = executor,
+                    user = userTracker.userHandle,
+            )
+        }
+    }
+
+    private fun bindToPanelInternal() {
         val currentSelection = controlsController.getPreferredSelection()
         val panels =
-            controlsListingController.getCurrentServices().filter { it.panelActivity != null }
-        if (
-            currentSelection is SelectedItem.PanelItem &&
+                controlsListingController.getCurrentServices().filter { it.panelActivity != null }
+        if (currentSelection is SelectedItem.PanelItem &&
                 panels.firstOrNull { it.componentName == currentSelection.componentName } != null
         ) {
             controlsController.bindComponentForPanel(currentSelection.componentName)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index ae5b799..64e2a2c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.os.Build
+import android.util.Log
 import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -34,6 +35,7 @@
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 
 /**
  * Encapsulates app state for the lock screen primary and alternate bouncer.
@@ -231,6 +233,7 @@
 
         primaryBouncerShow
             .logDiffsForTable(buffer, "", "PrimaryBouncerShow", false)
+            .onEach { Log.d(TAG, "Keyguard Bouncer is ${if (it) "showing" else "hiding."}") }
             .launchIn(applicationScope)
         primaryBouncerShowingSoon
             .logDiffsForTable(buffer, "", "PrimaryBouncerShowingSoon", false)
@@ -274,5 +277,6 @@
 
     companion object {
         private const val NOT_VISIBLE = -1L
+        private const val TAG = "KeyguardBouncerRepositoryImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index 0a3b3d3..e9184ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -95,8 +95,7 @@
     }
 
     val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
-    val show: Flow<Unit> = repository.primaryBouncerShow.filter { it }.map {}
-    val hide: Flow<Unit> = repository.primaryBouncerShow.filter { !it }.map {}
+    val isShowing: Flow<Boolean> = repository.primaryBouncerShow
     val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
     val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
     val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
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 5fcf105..468a6b5 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
@@ -109,36 +109,36 @@
                 try {
                     viewModel.setBouncerViewDelegate(delegate)
                     launch {
-                        viewModel.show.collect {
-                            // Reset Security Container entirely.
-                            securityContainerController.reinflateViewFlipper {
+                        viewModel.isShowing.collect { isShowing ->
+                            if (isShowing) {
                                 // Reset Security Container entirely.
-                                view.visibility = View.VISIBLE
+                                securityContainerController.reinflateViewFlipper {
+                                    // Reset Security Container entirely.
+                                    view.visibility = View.VISIBLE
+                                    securityContainerController.onBouncerVisibilityChanged(
+                                        /* isVisible= */ true
+                                    )
+                                    securityContainerController.showPrimarySecurityScreen(
+                                        /* turningOff= */ false
+                                    )
+                                    securityContainerController.appear()
+                                    securityContainerController.onResume(
+                                        KeyguardSecurityView.SCREEN_ON
+                                    )
+                                }
+                            } else {
+                                view.visibility = View.INVISIBLE
                                 securityContainerController.onBouncerVisibilityChanged(
-                                    /* isVisible= */ true
+                                    /* isVisible= */ false
                                 )
-                                securityContainerController.showPrimarySecurityScreen(
-                                    /* turningOff= */ false
-                                )
-                                securityContainerController.appear()
-                                securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+                                securityContainerController.cancelDismissAction()
+                                securityContainerController.reset()
+                                securityContainerController.onPause()
                             }
                         }
                     }
 
                     launch {
-                        viewModel.hide.collect {
-                            view.visibility = View.INVISIBLE
-                            securityContainerController.onBouncerVisibilityChanged(
-                                /* isVisible= */ false
-                            )
-                            securityContainerController.cancelDismissAction()
-                            securityContainerController.reset()
-                            securityContainerController.onPause()
-                        }
-                    }
-
-                    launch {
                         viewModel.startingToHide.collect {
                             securityContainerController.onStartingToHide()
                         }
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 0656c9b..9602888 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
@@ -40,11 +40,8 @@
     /** Can the user interact with the view? */
     val isInteractable: Flow<Boolean> = interactor.isInteractable
 
-    /** Observe whether bouncer is showing. */
-    val show: Flow<Unit> = interactor.show
-
-    /** Observe whether bouncer is hiding. */
-    val hide: Flow<Unit> = interactor.hide
+    /** Observe whether bouncer is showing or not. */
+    val isShowing: Flow<Boolean> = interactor.isShowing
 
     /** Observe whether bouncer is starting to hide. */
     val startingToHide: Flow<Unit> = interactor.startingToHide
@@ -70,8 +67,8 @@
     /** Observe whether we should update fps is showing. */
     val shouldUpdateSideFps: Flow<Unit> =
         merge(
-            interactor.hide,
-            interactor.show,
+            interactor.isShowing.map {},
+            interactor.startingToHide,
             interactor.startingDisappearAnimation.filterNotNull().map {}
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index e10d74d..54237ce 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -317,13 +317,16 @@
 
     /**
      * Returns the amount of translationY of the media container, during the current guided
-     * transformation, if running. If there is no guided transformation running, it will return 0.
+     * transformation, if running. If there is no guided transformation running, it will return -1.
      */
     fun getGuidedTransformationTranslationY(): Int {
         if (!isCurrentlyInGuidedTransformation()) {
             return -1
         }
-        val startHost = getHost(previousLocation) ?: return 0
+        val startHost = getHost(previousLocation)
+        if (startHost == null || !startHost.visible) {
+            return 0
+        }
         return targetBounds.top - startHost.currentBounds.top
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 6e185fe..ac7b4c2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -25,6 +25,7 @@
 import static com.android.systemui.shade.NotificationPanelViewController.FLING_HIDE;
 import static com.android.systemui.shade.NotificationPanelViewController.QS_PARALLAX_AMOUNT;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -246,6 +247,12 @@
     /** The duration of the notification bounds animation. */
     private long mNotificationBoundsAnimationDuration;
 
+    /** TODO(b/273591201): remove after bug resolved */
+    private int mLastClippingTopBound;
+    private int mLastNotificationsTopPadding;
+    private int mLastNotificationsClippingTopBound;
+    private int mLastNotificationsClippingTopBoundNssl;
+
     private final Region mInterceptRegion = new Region();
     /** The end bounds of a clipping animation. */
     private final Rect mClippingAnimationEndBounds = new Rect();
@@ -610,7 +617,7 @@
         float appearAmount = mNotificationStackScrollLayoutController
                 .calculateAppearFraction(mShadeExpandedHeight);
         float startHeight = -getExpansionHeight();
-        if (mBarState == StatusBarState.SHADE) {
+        if (mBarState == SHADE) {
             // Small parallax as we pull down and clip QS
             startHeight = -getExpansionHeight() * QS_PARALLAX_AMOUNT;
         }
@@ -1086,6 +1093,7 @@
                         mClippingAnimationEndBounds.left, fraction);
                 int animTop = (int) MathUtils.lerp(startTop,
                         mClippingAnimationEndBounds.top, fraction);
+                logClippingTopBound("interpolated top bound", top);
                 int animRight = (int) MathUtils.lerp(startRight,
                         mClippingAnimationEndBounds.right, fraction);
                 int animBottom = (int) MathUtils.lerp(startBottom,
@@ -1205,6 +1213,8 @@
             // the screen without clipping.
             return -mAmbientState.getStackTopMargin();
         } else {
+            logNotificationsClippingTopBound(qsTop,
+                    mNotificationStackScrollLayoutController.getTop());
             return qsTop - mNotificationStackScrollLayoutController.getTop();
         }
     }
@@ -1227,6 +1237,7 @@
     /** Calculate top padding for notifications */
     public float calculateNotificationsTopPadding(boolean isShadeExpanding,
             int keyguardNotificationStaticPadding, float expandedFraction) {
+        float topPadding;
         boolean keyguardShowing = mBarState == KEYGUARD;
         if (mSplitShadeEnabled) {
             return keyguardShowing
@@ -1243,19 +1254,27 @@
             int maxQsPadding = getMaxExpansionHeight();
             int max = keyguardShowing ? Math.max(
                     keyguardNotificationStaticPadding, maxQsPadding) : maxQsPadding;
-            return (int) MathUtils.lerp((float) getMinExpansionHeight(),
+            topPadding = (int) MathUtils.lerp((float) getMinExpansionHeight(),
                     (float) max, expandedFraction);
+            logNotificationsTopPadding("keyguard and expandImmediate", topPadding);
+            return topPadding;
         } else if (isSizeChangeAnimationRunning()) {
-            return Math.max((int) mSizeChangeAnimator.getAnimatedValue(),
+            topPadding = Math.max((int) mSizeChangeAnimator.getAnimatedValue(),
                     keyguardNotificationStaticPadding);
+            logNotificationsTopPadding("size change animation running", topPadding);
+            return topPadding;
         } else if (keyguardShowing) {
             // We can only do the smoother transition on Keyguard when we also are not collapsing
             // from a scrolled quick settings.
-            return MathUtils.lerp((float) keyguardNotificationStaticPadding,
+            topPadding = MathUtils.lerp((float) keyguardNotificationStaticPadding,
                     (float) (getMaxExpansionHeight()), computeExpansionFraction());
+            logNotificationsTopPadding("keyguard", topPadding);
+            return topPadding;
         } else {
-            return mQsFrameTranslateController.getNotificationsTopPadding(
+            topPadding = mQsFrameTranslateController.getNotificationsTopPadding(
                     mExpansionHeight, mNotificationStackScrollLayoutController);
+            logNotificationsTopPadding("default case", topPadding);
+            return topPadding;
         }
     }
 
@@ -1302,6 +1321,38 @@
                         - mAmbientState.getScrollY());
     }
 
+    /** TODO(b/273591201): remove after bug resolved */
+    private void logNotificationsTopPadding(String message, float rawPadding) {
+        int padding =  ((int) rawPadding / 10) * 10;
+        if (mBarState != KEYGUARD && padding != mLastNotificationsTopPadding && !mExpanded) {
+            mLastNotificationsTopPadding = padding;
+            mShadeLog.logNotificationsTopPadding(message, padding);
+        }
+    }
+
+    /** TODO(b/273591201): remove after bug resolved */
+    private void logClippingTopBound(String message, int top) {
+        top = (top / 10) * 10;
+        if (mBarState != KEYGUARD && mShadeExpandedFraction == 1
+                && top != mLastClippingTopBound && !mExpanded) {
+            mLastClippingTopBound = top;
+            mShadeLog.logClippingTopBound(message, top);
+        }
+    }
+
+    /** TODO(b/273591201): remove after bug resolved */
+    private void logNotificationsClippingTopBound(int top, int nsslTop) {
+        top = (top / 10) * 10;
+        nsslTop = (nsslTop / 10) * 10;
+        if (mBarState == SHADE && mShadeExpandedFraction == 1
+                && (top != mLastNotificationsClippingTopBound
+                || nsslTop != mLastNotificationsClippingTopBoundNssl) && !mExpanded) {
+            mLastNotificationsClippingTopBound = top;
+            mLastNotificationsClippingTopBoundNssl = nsslTop;
+            mShadeLog.logNotificationsClippingTopBound(top, nsslTop);
+        }
+    }
+
     private int calculateTopClippingBound(int qsPanelBottomY) {
         int top;
         if (mSplitShadeEnabled) {
@@ -1311,6 +1362,7 @@
                 // If we're transitioning, let's use the actual value. The else case
                 // can be wrong during transitions when waiting for the keyguard to unlock
                 top = mTransitionToFullShadePosition;
+                logClippingTopBound("set while transitioning to full shade", top);
             } else {
                 final float notificationTop = getEdgePosition();
                 if (mBarState == KEYGUARD) {
@@ -1319,8 +1371,10 @@
                         // this should go away once we unify the stackY position and don't have
                         // to do this min anymore below.
                         top = qsPanelBottomY;
+                        logClippingTopBound("bypassing keyguard", top);
                     } else {
                         top = (int) Math.min(qsPanelBottomY, notificationTop);
+                        logClippingTopBound("keyguard default case", top);
                     }
                 } else {
                     top = (int) notificationTop;
@@ -1328,12 +1382,14 @@
             }
             // TODO (b/265193930): remove dependency on NPVC
             top += mPanelViewControllerLazy.get().getOverStretchAmount();
+            logClippingTopBound("including overstretch", top);
             // Correction for instant expansion caused by HUN pull down/
             float minFraction = mPanelViewControllerLazy.get().getMinFraction();
             if (minFraction > 0f && minFraction < 1f) {
                 float realFraction = (mShadeExpandedFraction
                         - minFraction) / (1f - minFraction);
                 top *= MathUtils.saturate(realFraction / minFraction);
+                logClippingTopBound("after adjusted fraction", top);
             }
         }
         return top;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index d34e127..da4944c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -280,4 +280,40 @@
             { "Split shade state changed: split shade ${if (bool1) "enabled" else "disabled"}" }
         )
     }
+
+    fun logNotificationsTopPadding(message: String, padding: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                int1 = padding
+            },
+            { "QSC NotificationsTopPadding $str1: $int1"}
+        )
+    }
+
+    fun logClippingTopBound(message: String, top: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                int1 = top
+            },
+            { "QSC ClippingTopBound $str1: $int1" }
+        )
+    }
+
+    fun logNotificationsClippingTopBound(top: Int, nsslTop: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                int1 = top
+                int2 = nsslTop
+            },
+            { "QSC NotificationsClippingTopBound set to $int1 - $int2" }
+        )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
index 94cf384..9254617 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
@@ -50,7 +50,7 @@
         displayMetrics.widthPixels = 1000;
         displayMetrics.heightPixels = 1000;
         mDataProvider = new FalsingDataProvider(
-                displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
+                displayMetrics, mBatteryController, mFoldStateListener, mDockManager, false);
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
index 8eadadf..7e06680 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
@@ -54,18 +54,18 @@
     @Mock
     private FoldStateListener mFoldStateListener;
     private final DockManagerFake mDockManager = new DockManagerFake();
+    private DisplayMetrics mDisplayMetrics;
 
     @Before
     public void setup() {
         super.setup();
         MockitoAnnotations.initMocks(this);
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        displayMetrics.xdpi = 100;
-        displayMetrics.ydpi = 100;
-        displayMetrics.widthPixels = 1000;
-        displayMetrics.heightPixels = 1000;
-        mDataProvider = new FalsingDataProvider(
-                displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
+        mDisplayMetrics = new DisplayMetrics();
+        mDisplayMetrics.xdpi = 100;
+        mDisplayMetrics.ydpi = 100;
+        mDisplayMetrics.widthPixels = 1000;
+        mDisplayMetrics.heightPixels = 1000;
+        mDataProvider = createWithFoldCapability(false);
     }
 
     @After
@@ -345,20 +345,42 @@
     }
 
     @Test
-    public void test_FoldedState_Folded() {
+    public void test_UnfoldedState_Folded() {
+        FalsingDataProvider falsingDataProvider = createWithFoldCapability(true);
         when(mFoldStateListener.getFolded()).thenReturn(true);
-        assertThat(mDataProvider.isUnfolded()).isFalse();
+        assertThat(falsingDataProvider.isUnfolded()).isFalse();
     }
 
     @Test
-    public void test_FoldedState_Unfolded() {
+    public void test_UnfoldedState_Unfolded() {
+        FalsingDataProvider falsingDataProvider = createWithFoldCapability(true);
         when(mFoldStateListener.getFolded()).thenReturn(false);
-        assertThat(mDataProvider.isUnfolded()).isTrue();
+        assertThat(falsingDataProvider.isUnfolded()).isTrue();
     }
 
     @Test
-    public void test_FoldedState_NotFoldable() {
+    public void test_Nonfoldabled_TrueFoldState() {
+        FalsingDataProvider falsingDataProvider = createWithFoldCapability(false);
+        when(mFoldStateListener.getFolded()).thenReturn(true);
+        assertThat(falsingDataProvider.isUnfolded()).isFalse();
+    }
+
+    @Test
+    public void test_Nonfoldabled_FalseFoldState() {
+        FalsingDataProvider falsingDataProvider = createWithFoldCapability(false);
+        when(mFoldStateListener.getFolded()).thenReturn(false);
+        assertThat(falsingDataProvider.isUnfolded()).isFalse();
+    }
+
+    @Test
+    public void test_Nonfoldabled_NullFoldState() {
+        FalsingDataProvider falsingDataProvider = createWithFoldCapability(true);
         when(mFoldStateListener.getFolded()).thenReturn(null);
-        assertThat(mDataProvider.isUnfolded()).isFalse();
+        assertThat(falsingDataProvider.isUnfolded()).isFalse();
+    }
+
+    private FalsingDataProvider createWithFoldCapability(boolean foldable) {
+        return new FalsingDataProvider(
+                mDisplayMetrics, mBatteryController, mFoldStateListener, mDockManager, foldable);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
index bd7e98e..8f65fc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
@@ -17,13 +17,18 @@
 
 package com.android.systemui.controls.start
 
+import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
 import android.content.pm.ApplicationInfo
 import android.content.pm.ServiceInfo
+import android.os.UserManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.dagger.ControlsComponent
@@ -34,14 +39,20 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.never
@@ -58,6 +69,8 @@
     @Mock private lateinit var controlsListingController: ControlsListingController
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
 
     private val preferredPanelsRepository = FakeSelectedComponentRepository()
 
@@ -67,13 +80,27 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(authorizedPanelsRepository.getPreferredPackages()).thenReturn(setOf())
+        whenever(userManager.isUserUnlocked(anyInt())).thenReturn(true)
 
         fakeExecutor = FakeExecutor(FakeSystemClock())
     }
 
     @Test
     fun testDisabledNothingIsCalled() {
-        createStartable(enabled = false).start()
+        createStartable(enabled = false).apply {
+            start()
+            onBootCompleted()
+        }
+
+        verifyZeroInteractions(controlsController, controlsListingController, userTracker)
+    }
+
+    @Test
+    fun testNothingCalledOnStart() {
+        createStartable(enabled = true).start()
+
+        fakeExecutor.advanceClockToLast()
+        fakeExecutor.runAllReady()
 
         verifyZeroInteractions(controlsController, controlsListingController, userTracker)
     }
@@ -84,7 +111,7 @@
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).setPreferredSelection(any())
@@ -97,7 +124,7 @@
         `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
         setUpControlsListingControls(emptyList())
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).setPreferredSelection(any())
@@ -111,7 +138,7 @@
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false))
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).setPreferredSelection(any())
@@ -126,7 +153,7 @@
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).setPreferredSelection(any())
@@ -140,7 +167,7 @@
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
@@ -158,7 +185,7 @@
             )
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
@@ -176,32 +203,78 @@
             )
         setUpControlsListingControls(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController).setPreferredSelection(listings[1].toPanelItem())
     }
 
     @Test
-    fun testPreferredSelectionIsPanel_bindOnStart() {
+    fun testPreferredSelectionIsPanel_bindOnBoot() {
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
         setUpControlsListingControls(listings)
         `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL)
     }
 
     @Test
+    fun testPreferredSelectionIsPanel_userNotUnlocked_notBind() {
+        whenever(userManager.isUserUnlocked(anyInt())).thenReturn(false)
+
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        setUpControlsListingControls(listings)
+        `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
+
+        createStartable(enabled = true).onBootCompleted()
+        fakeExecutor.runAllReady()
+
+        verify(controlsController, never()).bindComponentForPanel(TEST_COMPONENT_PANEL)
+    }
+
+    @Test
+    fun testPreferredSelectionIsPanel_userNotUnlocked_broadcastRegistered_broadcastSentBinds() {
+        whenever(userManager.isUserUnlocked(anyInt())).thenReturn(false)
+
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        setUpControlsListingControls(listings)
+        `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
+
+        createStartable(enabled = true).onBootCompleted()
+        fakeExecutor.runAllReady()
+
+        val intentFilterCaptor = argumentCaptor<IntentFilter>()
+        val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+
+        verify(broadcastDispatcher)
+            .registerReceiver(
+                capture(receiverCaptor),
+                capture(intentFilterCaptor),
+                eq(fakeExecutor),
+                nullable(),
+                anyInt(),
+                nullable()
+            )
+        assertThat(intentFilterCaptor.value.matchAction(Intent.ACTION_USER_UNLOCKED)).isTrue()
+
+        // User is unlocked
+        whenever(userManager.isUserUnlocked(anyInt())).thenReturn(true)
+        receiverCaptor.value.onReceive(mock(), Intent(Intent.ACTION_USER_UNLOCKED))
+
+        verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL)
+    }
+
+    @Test
     fun testPreferredSelectionPanel_listingNoPanel_notBind() {
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
         setUpControlsListingControls(listings)
         `when`(controlsController.getPreferredSelection())
             .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL))
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).bindComponentForPanel(any())
@@ -213,7 +286,7 @@
         setUpControlsListingControls(listings)
         `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
         fakeExecutor.runAllReady()
 
         verify(controlsController, never()).bindComponentForPanel(any())
@@ -228,7 +301,7 @@
         val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
         `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
 
-        createStartable(enabled = true).start()
+        createStartable(enabled = true).onBootCompleted()
 
         verify(controlsController, never()).setPreferredSelection(any())
     }
@@ -258,6 +331,8 @@
             userTracker,
             authorizedPanelsRepository,
             preferredPanelsRepository,
+            userManager,
+            broadcastDispatcher,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index 2ab1b99..9cd2220 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -125,4 +125,26 @@
         assertThat(sideFpsIsShowing).isEqualTo(true)
         job.cancel()
     }
+
+    @Test
+    fun isShowing() = runTest {
+        var isShowing: Boolean? = null
+        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
+        repository.setPrimaryShow(true)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(isShowing).isEqualTo(true)
+        job.cancel()
+    }
+
+    @Test
+    fun isNotShowing() = runTest {
+        var isShowing: Boolean? = null
+        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
+        repository.setPrimaryShow(false)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(isShowing).isEqualTo(false)
+        job.cancel()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index feb429d..eb78ded 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -389,6 +389,15 @@
     }
 
     @Test
+    fun getGuidedTransformationTranslationY_previousHostInvisible_returnsZero() {
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(false)
+
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isEqualTo(0)
+    }
+
+    @Test
     fun isCurrentlyInGuidedTransformation_hostsVisible_returnsTrue() {
         goToLockscreen()
         enterGuidedTransformation()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt
index 0e7e039..4989a21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt
@@ -18,6 +18,7 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.systemui.SysuiTestCase
 import org.junit.Before
 import org.junit.Test
@@ -27,23 +28,27 @@
 @SmallTest
 class RemoteUnfoldTransitionReceiverTest : SysuiTestCase() {
 
-    private val progressProvider = RemoteUnfoldTransitionReceiver { it.run() }
+    private val progressProvider =
+        RemoteUnfoldTransitionReceiver(useReceivingFilter = true) { runOnMainSync(it) }
+    private val progressProviderWithoutFilter =
+        RemoteUnfoldTransitionReceiver(useReceivingFilter = false) { it.run() }
     private val listener = TestUnfoldProgressListener()
 
     @Before
     fun setUp() {
         progressProvider.addCallback(listener)
+        progressProviderWithoutFilter.addCallback(listener)
     }
 
     @Test
-    fun onTransitionStarted_propagated() {
+    fun onTransitionStarted_withFilter_propagated() {
         progressProvider.onTransitionStarted()
 
         listener.assertStarted()
     }
 
     @Test
-    fun onTransitionProgress_propagated() {
+    fun onTransitionProgress_withFilter_propagated() {
         progressProvider.onTransitionStarted()
 
         progressProvider.onTransitionProgress(0.5f)
@@ -52,7 +57,7 @@
     }
 
     @Test
-    fun onTransitionEnded_propagated() {
+    fun onTransitionEnded_withFilter_propagated() {
         progressProvider.onTransitionStarted()
         progressProvider.onTransitionProgress(0.5f)
 
@@ -62,11 +67,52 @@
     }
 
     @Test
-    fun onTransitionStarted_afterCallbackRemoved_notPropagated() {
+    fun onTransitionStarted_withFilter_afterCallbackRemoved_notPropagated() {
         progressProvider.removeCallback(listener)
 
         progressProvider.onTransitionStarted()
 
         listener.assertNotStarted()
     }
+
+    @Test
+    fun onTransitionStarted_withoutFilter_propagated() {
+        progressProviderWithoutFilter.onTransitionStarted()
+
+        listener.assertStarted()
+    }
+
+    @Test
+    fun onTransitionProgress_withoutFilter_propagated() {
+        progressProviderWithoutFilter.onTransitionStarted()
+
+        progressProviderWithoutFilter.onTransitionProgress(0.5f)
+
+        listener.assertLastProgress(0.5f)
+    }
+
+    @Test
+    fun onTransitionEnded_withoutFilter_propagated() {
+        progressProviderWithoutFilter.onTransitionStarted()
+        progressProviderWithoutFilter.onTransitionProgress(0.5f)
+
+        progressProviderWithoutFilter.onTransitionFinished()
+
+        listener.ensureTransitionFinished()
+    }
+
+    @Test
+    fun onTransitionStarted_withoutFilter_afterCallbackRemoved_notPropagated() {
+        progressProviderWithoutFilter.removeCallback(listener)
+
+        progressProviderWithoutFilter.onTransitionStarted()
+
+        listener.assertNotStarted()
+    }
+
+    private fun runOnMainSync(f: Runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
+        // Sleep as the animator used from the filter has a callback that happens at every frame.
+        Thread.sleep(60)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
index f653207..1ce2572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
@@ -126,7 +126,7 @@
         }
 
         fun assertLastProgress(progress: Float) {
-            assertThat(progressHistory.last()).isEqualTo(progress)
+            assertThat(progressHistory.last()).isWithin(1.0E-4F).of(progress)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
new file mode 100644
index 0000000..f14009aa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.progress
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.systemui.SysuiTestCase
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldRemoteFilterTest : SysuiTestCase() {
+    private val listener = TestUnfoldProgressListener()
+
+    private val progressProvider = UnfoldRemoteFilter(listener)
+
+    @Test
+    fun onTransitionStarted_propagated() {
+        runOnMainThreadWithInterval({ progressProvider.onTransitionStarted() })
+        listener.assertStarted()
+    }
+
+    @Test
+    fun onTransitionProgress_withInterval_propagated() {
+        runOnMainThreadWithInterval(
+            { progressProvider.onTransitionStarted() },
+            { progressProvider.onTransitionProgress(0.5f) }
+        )
+
+        listener.assertLastProgress(0.5f)
+    }
+
+    @Test
+    fun onTransitionEnded_propagated() {
+        runOnMainThreadWithInterval(
+            { progressProvider.onTransitionStarted() },
+            { progressProvider.onTransitionProgress(0.5f) },
+            { progressProvider.onTransitionFinished() },
+        )
+
+        listener.ensureTransitionFinished()
+    }
+
+    private fun runOnMainThreadWithInterval(
+        vararg blocks: () -> Unit,
+        interval: Duration = 60.milliseconds
+    ) {
+        blocks.forEach {
+            InstrumentationRegistry.getInstrumentation().runOnMainSync { it() }
+            Thread.sleep(interval.inWholeMilliseconds)
+        }
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt
index b395d9c..a639df5 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.unfold
 
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.systemui.unfold.dagger.UseReceivingFilter
 import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
 import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
 import dagger.Module
@@ -42,4 +43,6 @@
         remoteReceiver.addCallback(traceListener)
         return Optional.of(remoteReceiver)
     }
+
+    @Provides @UseReceivingFilter fun useReceivingFilter(): Boolean = true
 }
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt
new file mode 100644
index 0000000..60e9307
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.unfold.dagger
+
+import javax.inject.Qualifier
+
+/** Annotates whether to use a filter in [RemoteUnfoldTransitionReceiver]. */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UseReceivingFilter
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt
index 5e4bcc9..b2c26c4 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.unfold.progress
 
+import android.util.Log
+import androidx.annotation.BinderThread
+import androidx.annotation.FloatRange
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.dagger.UseReceivingFilter
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -30,21 +34,40 @@
  */
 class RemoteUnfoldTransitionReceiver
 @Inject
-constructor(@UnfoldMain private val executor: Executor) :
-    UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
+constructor(
+    @UseReceivingFilter useReceivingFilter: Boolean,
+    @UnfoldMain private val executor: Executor
+) : UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
 
     private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
+    private val outputProgressListener = ProcessedProgressListener()
+    private val filter: TransitionProgressListener? =
+        if (useReceivingFilter) {
+            UnfoldRemoteFilter(outputProgressListener)
+        } else {
+            null
+        }
 
+    @BinderThread
     override fun onTransitionStarted() {
-        executor.execute { listeners.forEach { it.onTransitionStarted() } }
+        executor.execute {
+            filter?.onTransitionStarted() ?: outputProgressListener.onTransitionStarted()
+        }
     }
 
+    @BinderThread
     override fun onTransitionProgress(progress: Float) {
-        executor.execute { listeners.forEach { it.onTransitionProgress(progress) } }
+        executor.execute {
+            filter?.onTransitionProgress(progress)
+                ?: outputProgressListener.onTransitionProgress(progress)
+        }
     }
 
+    @BinderThread
     override fun onTransitionFinished() {
-        executor.execute { listeners.forEach { it.onTransitionFinished() } }
+        executor.execute {
+            filter?.onTransitionFinished() ?: outputProgressListener.onTransitionFinished()
+        }
     }
 
     override fun addCallback(listener: TransitionProgressListener) {
@@ -58,4 +81,30 @@
     override fun destroy() {
         listeners.clear()
     }
+
+    private inner class ProcessedProgressListener : TransitionProgressListener {
+        override fun onTransitionStarted() {
+            log { "onTransitionStarted" }
+            listeners.forEach { it.onTransitionStarted() }
+        }
+
+        override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+            log { "onTransitionProgress" }
+            listeners.forEach { it.onTransitionProgress(progress) }
+        }
+
+        override fun onTransitionFinished() {
+            log { "onTransitionFinished" }
+            listeners.forEach { it.onTransitionFinished() }
+        }
+    }
+
+    private fun log(s: () -> String) {
+        if (DEBUG) {
+            Log.d(TAG, s())
+        }
+    }
 }
+
+private const val TAG = "RemoteUnfoldReceiver"
+private val DEBUG = false
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt
new file mode 100644
index 0000000..0b019d1
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt
@@ -0,0 +1,85 @@
+package com.android.systemui.unfold.progress
+
+import android.os.Trace
+import android.util.Log
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+
+/**
+ * Makes progress received from other processes resilient to jank.
+ *
+ * Sender and receiver processes might have different frame-rates. If the sending process is
+ * dropping a frame due to jank (or generally because it's main thread is too busy), we don't want
+ * the receiving process to drop progress frames as well. For this reason, a spring animator pass
+ * (with very high stiffness) is applied to the incoming progress. This adds a small delay to the
+ * progress (~30ms), but guarantees an always smooth animation on the receiving end.
+ */
+class UnfoldRemoteFilter(
+    private val listener: UnfoldTransitionProgressProvider.TransitionProgressListener
+) : UnfoldTransitionProgressProvider.TransitionProgressListener {
+
+    private val springAnimation =
+        SpringAnimation(this, AnimationProgressProperty).apply {
+            spring =
+                SpringForce().apply {
+                    dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+                    stiffness = 100_000f
+                    finalPosition = 1.0f
+                }
+            setMinValue(0f)
+            setMaxValue(1f)
+            minimumVisibleChange = 0.001f
+        }
+
+    private var inProgress = false
+
+    private var processedProgress: Float = 0.0f
+        set(newProgress) {
+            if (inProgress) {
+                logCounter({ "$TAG#filtered_progress" }, newProgress)
+                listener.onTransitionProgress(newProgress)
+            } else {
+                Log.e(TAG, "Filtered progress received received while animation not in progress.")
+            }
+            field = newProgress
+        }
+
+    override fun onTransitionStarted() {
+        listener.onTransitionStarted()
+        inProgress = true
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        logCounter({ "$TAG#plain_remote_progress" }, progress)
+        if (inProgress) {
+            springAnimation.animateToFinalPosition(progress)
+        } else {
+            Log.e(TAG, "Progress received while not in progress.")
+        }
+    }
+
+    override fun onTransitionFinished() {
+        inProgress = false
+        listener.onTransitionFinished()
+    }
+
+    private object AnimationProgressProperty :
+        FloatPropertyCompat<UnfoldRemoteFilter>("UnfoldRemoteFilter") {
+
+        override fun setValue(provider: UnfoldRemoteFilter, value: Float) {
+            provider.processedProgress = value
+        }
+
+        override fun getValue(provider: UnfoldRemoteFilter): Float = provider.processedProgress
+    }
+    private fun logCounter(name: () -> String, progress: Float) {
+        if (DEBUG) {
+            Trace.setCounter(name(), (progress * 100).toLong())
+        }
+    }
+}
+
+private val TAG = "UnfoldRemoteFilter"
+private val DEBUG = false
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index a9e7d4b..c9cdba9 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -106,12 +106,12 @@
     };
 
     // Spatializer state machine
-    private static final int STATE_UNINITIALIZED = 0;
-    private static final int STATE_NOT_SUPPORTED = 1;
-    private static final int STATE_DISABLED_UNAVAILABLE = 3;
-    private static final int STATE_ENABLED_UNAVAILABLE = 4;
-    private static final int STATE_ENABLED_AVAILABLE = 5;
-    private static final int STATE_DISABLED_AVAILABLE = 6;
+    /*package*/ static final int STATE_UNINITIALIZED = 0;
+    /*package*/ static final int STATE_NOT_SUPPORTED = 1;
+    /*package*/ static final int STATE_DISABLED_UNAVAILABLE = 3;
+    /*package*/ static final int STATE_ENABLED_UNAVAILABLE = 4;
+    /*package*/ static final int STATE_ENABLED_AVAILABLE = 5;
+    /*package*/ static final int STATE_DISABLED_AVAILABLE = 6;
     private int mState = STATE_UNINITIALIZED;
 
     private boolean mFeatureEnabled = false;
@@ -147,9 +147,9 @@
             .setSampleRate(48000)
             .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
             .build();
-    // device array to store the routing for the default attributes and format, size 1 because
-    // media is never expected to be duplicated
-    private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1];
+    // device array to store the routing for the default attributes and format, initialized to
+    // an empty list as routing hasn't been established yet
+    private static ArrayList<AudioDeviceAttributes> sRoutingDevices = new ArrayList<>(0);
 
     //---------------------------------------------------------------
     // audio device compatibility / enabled
@@ -184,11 +184,6 @@
         SADeviceState.sHeadTrackingEnabledDefault = headTrackingEnabledDefault;
     }
 
-    synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) {
-        mBinauralSupported = hasBinaural;
-        mTransauralSupported = hasTransaural;
-    }
-
     synchronized void init(boolean effectExpected, @Nullable String settings) {
         loglogi("init effectExpected=" + effectExpected);
         if (!effectExpected) {
@@ -322,8 +317,7 @@
             return;
         }
         mState = STATE_DISABLED_UNAVAILABLE;
-        mASA.getDevicesForAttributes(
-                DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES);
+        sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES);
         // note at this point mSpat is still not instantiated
     }
 
@@ -365,34 +359,35 @@
             case STATE_DISABLED_AVAILABLE:
                 break;
         }
-        mASA.getDevicesForAttributes(
-                DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES);
+
+        sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES);
 
         // check validity of routing information
-        if (ROUTING_DEVICES[0] == null) {
-            logloge("onRoutingUpdated: device is null, no Spatial Audio");
+        if (sRoutingDevices.isEmpty()) {
+            logloge("onRoutingUpdated: no device, no Spatial Audio");
             setDispatchAvailableState(false);
             // not changing the spatializer level as this is likely a transient state
             return;
         }
+        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
 
         // is media routed to a new device?
-        if (isWireless(ROUTING_DEVICES[0].getType())) {
-            addWirelessDeviceIfNew(ROUTING_DEVICES[0]);
+        if (isWireless(currentDevice.getType())) {
+            addWirelessDeviceIfNew(currentDevice);
         }
 
         // find if media device enabled / available
-        final Pair<Boolean, Boolean> enabledAvailable = evaluateState(ROUTING_DEVICES[0]);
+        final Pair<Boolean, Boolean> enabledAvailable = evaluateState(currentDevice);
 
         boolean able = false;
         if (enabledAvailable.second) {
             // available for Spatial audio, check w/ effect
-            able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES);
+            able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, sRoutingDevices);
             loglogi("onRoutingUpdated: can spatialize media 5.1:" + able
-                    + " on device:" + ROUTING_DEVICES[0]);
+                    + " on device:" + currentDevice);
             setDispatchAvailableState(able);
         } else {
-            loglogi("onRoutingUpdated: device:" + ROUTING_DEVICES[0]
+            loglogi("onRoutingUpdated: device:" + currentDevice
                     + " not available for Spatial Audio");
             setDispatchAvailableState(false);
         }
@@ -400,10 +395,10 @@
         boolean enabled = able && enabledAvailable.first;
         if (enabled) {
             loglogi("Enabling Spatial Audio since enabled for media device:"
-                    + ROUTING_DEVICES[0]);
+                    + currentDevice);
         } else {
             loglogi("Disabling Spatial Audio since disabled for media device:"
-                    + ROUTING_DEVICES[0]);
+                    + currentDevice);
         }
         if (mSpat != null) {
             byte level = enabled ? (byte) Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL
@@ -736,9 +731,13 @@
     }
 
     private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
-            @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) {
-        if (isDeviceCompatibleWithSpatializationModes(devices[0])) {
-            return AudioSystem.canBeSpatialized(attributes, format, devices);
+            @NonNull AudioFormat format, @NonNull ArrayList<AudioDeviceAttributes> devices) {
+        if (devices.isEmpty()) {
+            return false;
+        }
+        if (isDeviceCompatibleWithSpatializationModes(devices.get(0))) {
+            AudioDeviceAttributes[] devArray = new AudioDeviceAttributes[devices.size()];
+            return AudioSystem.canBeSpatialized(attributes, format, devices.toArray(devArray));
         }
         return false;
     }
@@ -1014,10 +1013,13 @@
                 logd("canBeSpatialized false due to usage:" + attributes.getUsage());
                 return false;
         }
-        AudioDeviceAttributes[] devices = new AudioDeviceAttributes[1];
+
         // going through adapter to take advantage of routing cache
-        mASA.getDevicesForAttributes(
-                attributes, false /* forVolume */).toArray(devices);
+        final ArrayList<AudioDeviceAttributes> devices = getRoutingDevices(attributes);
+        if (devices.isEmpty()) {
+            logloge("canBeSpatialized got no device for " + attributes);
+            return false;
+        }
         final boolean able = canBeSpatializedOnDevice(attributes, format, devices);
         logd("canBeSpatialized usage:" + attributes.getUsage()
                 + " format:" + format.toLogFriendlyString() + " returning " + able);
@@ -1148,8 +1150,13 @@
         logDeviceState(deviceState, "setHeadTrackerEnabled");
 
         // check current routing to see if it affects the headtracking mode
-        if (ROUTING_DEVICES[0] != null && ROUTING_DEVICES[0].getType() == ada.getType()
-                && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
+        if (sRoutingDevices.isEmpty()) {
+            logloge("setHeadTrackerEnabled: no device, bailing");
+            return;
+        }
+        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
+        if (currentDevice.getType() == ada.getType()
+                && currentDevice.getAddress().equals(ada.getAddress())) {
             setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled
                     : Spatializer.HEAD_TRACKING_MODE_DISABLED);
             if (enabled && !mHeadTrackerAvailable) {
@@ -1706,10 +1713,11 @@
 
     private int getHeadSensorHandleUpdateTracker() {
         int headHandle = -1;
-        final AudioDeviceAttributes currentDevice = ROUTING_DEVICES[0];
-        if (currentDevice == null) {
+        if (sRoutingDevices.isEmpty()) {
+            logloge("getHeadSensorHandleUpdateTracker: no device, no head tracker");
             return headHandle;
         }
+        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
         UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice);
         // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
         // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
@@ -1743,6 +1751,23 @@
         return screenHandle;
     }
 
+    /**
+     * Returns routing for the given attributes
+     * @param aa AudioAttributes whose routing is being queried
+     * @return a non-null never-empty list of devices. If the routing query failed, the list
+     *     will contain null.
+     */
+    private @NonNull ArrayList<AudioDeviceAttributes> getRoutingDevices(AudioAttributes aa) {
+        final ArrayList<AudioDeviceAttributes> devices = mASA.getDevicesForAttributes(
+                aa, false /* forVolume */);
+        for (AudioDeviceAttributes ada : devices) {
+            if (ada == null) {
+                // invalid entry, reject this routing query by returning an empty list
+                return new ArrayList<>(0);
+            }
+        }
+        return devices;
+    }
 
     private static void loglogi(String msg) {
         AudioService.sSpatialLogger.loglogi(msg, TAG);
@@ -1759,4 +1784,13 @@
     /*package*/ void clearSADevices() {
         mSADevices.clear();
     }
+
+    /*package*/ synchronized void forceStateForTest(int state) {
+        mState = state;
+    }
+
+    /*package*/ synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) {
+        mBinauralSupported = hasBinaural;
+        mTransauralSupported = hasTransaural;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a8626df..acadf4a0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -100,6 +100,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
 import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET;
 import static android.os.Build.VERSION_CODES.HONEYCOMB;
@@ -827,6 +828,13 @@
     /** Whether the input to this activity will be dropped during the current playing animation. */
     private boolean mIsInputDroppedForAnimation;
 
+    /**
+     * Whether the application has desk mode resources. Calculated and cached when
+     * {@link #hasDeskResources()} is called.
+     */
+    @Nullable
+    private Boolean mHasDeskResources;
+
     boolean mHandleExitSplashScreen;
     @TransferSplashScreenState
     int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
@@ -9490,7 +9498,14 @@
             configChanged |= CONFIG_UI_MODE;
         }
 
-        return (changes&(~configChanged)) != 0;
+        // TODO(b/274944389): remove workaround after long-term solution is implemented
+        // Don't restart due to desk mode change if the app does not have desk resources.
+        if (mWmService.mSkipActivityRelaunchWhenDocking && onlyDeskInUiModeChanged(changesConfig)
+                && !hasDeskResources()) {
+            configChanged |= CONFIG_UI_MODE;
+        }
+
+        return (changes & (~configChanged)) != 0;
     }
 
     /**
@@ -9503,6 +9518,50 @@
             != isInVrUiMode(lastReportedConfig));
     }
 
+    /**
+     * Returns true if the uiMode configuration changed, and desk mode
+     * ({@link android.content.res.Configuration#UI_MODE_TYPE_DESK}) was the only change to uiMode.
+     */
+    private boolean onlyDeskInUiModeChanged(Configuration lastReportedConfig) {
+        final Configuration currentConfig = getConfiguration();
+
+        boolean deskModeChanged = isInDeskUiMode(currentConfig) != isInDeskUiMode(
+                lastReportedConfig);
+        // UI mode contains fields other than the UI mode type, so determine if any other fields
+        // changed.
+        boolean uiModeOtherFieldsChanged =
+                (currentConfig.uiMode & ~UI_MODE_TYPE_MASK) != (lastReportedConfig.uiMode
+                        & ~UI_MODE_TYPE_MASK);
+
+        return deskModeChanged && !uiModeOtherFieldsChanged;
+    }
+
+    /**
+     * Determines whether or not the application has desk mode resources.
+     */
+    boolean hasDeskResources() {
+        if (mHasDeskResources != null) {
+            // We already determined this, return the cached value.
+            return mHasDeskResources;
+        }
+
+        mHasDeskResources = false;
+        try {
+            Resources packageResources = mAtmService.mContext.createPackageContextAsUser(
+                    packageName, 0, UserHandle.of(mUserId)).getResources();
+            for (Configuration sizeConfiguration :
+                    packageResources.getSizeAndUiModeConfigurations()) {
+                if (isInDeskUiMode(sizeConfiguration)) {
+                    mHasDeskResources = true;
+                    break;
+                }
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "Exception thrown during checking for desk resources " + this, e);
+        }
+        return mHasDeskResources;
+    }
+
     private int getConfigurationChanges(Configuration lastReportedConfig) {
         // Determine what has changed.  May be nothing, if this is a config that has come back from
         // the app after going idle.  In that case we just want to leave the official config object
@@ -9798,6 +9857,10 @@
         return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET;
     }
 
+    private static boolean isInDeskUiMode(Configuration config) {
+        return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_DESK;
+    }
+
     String getProcessName() {
         return info.applicationInfo.processName;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 41eb2c9..fb72d6c 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -378,10 +378,7 @@
             // Checking whether an activity in fullscreen rather than the task as this camera
             // compat treatment doesn't cover activity embedding.
             if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
-                if (topActivity.mLetterboxUiController
-                        .isOverrideOrientationOnlyForCameraEnabled()) {
-                    topActivity.recomputeConfiguration();
-                }
+                topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
                 mDisplayContent.updateOrientation();
                 return;
             }
@@ -447,9 +444,7 @@
                     || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
                 return;
             }
-            if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
-                topActivity.recomputeConfiguration();
-            }
+            topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
             mDisplayContent.updateOrientation();
         }
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index ec04894..6936a2d 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -53,6 +53,9 @@
      */
     static final float MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO = 1.0f;
 
+    /** Letterboxed app window position multiplier indicating center position. */
+    static final float LETTERBOX_POSITION_MULTIPLIER_CENTER = 0.5f;
+
     /** Enum for Letterbox background type. */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({LETTERBOX_BACKGROUND_SOLID_COLOR, LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND,
@@ -212,6 +215,10 @@
     // otherwise the apps get blacked out when they are resumed and do not have focus yet.
     private boolean mIsCompatFakeFocusEnabled;
 
+    // Whether should use split screen aspect ratio for the activity when camera compat treatment
+    // is enabled and activity is connected to the camera in fullscreen.
+    private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled;
+
     // Whether camera compatibility treatment is enabled.
     // See DisplayRotationCompatPolicy for context.
     private final boolean mIsCameraCompatTreatmentEnabled;
@@ -300,6 +307,8 @@
                 R.bool.config_letterboxIsEnabledForTranslucentActivities);
         mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
                 R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
+        mIsCameraCompatSplitScreenAspectRatioEnabled = mContext.getResources().getBoolean(
+                R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
         mIsCompatFakeFocusEnabled = mContext.getResources().getBoolean(
                 R.bool.config_isCompatFakeFocusEnabled);
         mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
@@ -1122,6 +1131,14 @@
         return mIsPolicyForIgnoringRequestedOrientationEnabled;
     }
 
+    /**
+     * Whether should use split screen aspect ratio for the activity when camera compat treatment
+     * is enabled and activity is connected to the camera in fullscreen.
+     */
+    boolean isCameraCompatSplitScreenAspectRatioEnabled() {
+        return mIsCameraCompatSplitScreenAspectRatioEnabled;
+    }
+
     /** Whether camera compatibility treatment is enabled. */
     boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
         return mIsCameraCompatTreatmentEnabled && (!checkDeviceConfig
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 4d0c768..da4d3cb 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -77,6 +77,7 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
@@ -242,6 +243,8 @@
 
     private boolean mIsRelauchingAfterRequestedOrientationChanged;
 
+    private boolean mDoubleTapEvent;
+
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
         mLetterboxConfiguration = wmService.mLetterboxConfiguration;
         // Given activityRecord may not be fully constructed since LetterboxUiController
@@ -398,13 +401,7 @@
                         + mActivityRecord);
                 return true;
             }
-            DisplayContent displayContent = mActivityRecord.mDisplayContent;
-            if (displayContent == null) {
-                return false;
-            }
-            if (displayContent.mDisplayRotationCompatPolicy != null
-                    && displayContent.mDisplayRotationCompatPolicy
-                    .isTreatmentEnabledForActivity(mActivityRecord)) {
+            if (isCameraCompatTreatmentActive()) {
                 Slog.w(TAG, "Ignoring orientation update to "
                         + screenOrientationToString(requestedOrientation)
                         + " due to camera compat treatment for " + mActivityRecord);
@@ -642,6 +639,16 @@
                 mBooleanPropertyCameraCompatAllowForceRotation);
     }
 
+    private boolean isCameraCompatTreatmentActive() {
+        DisplayContent displayContent = mActivityRecord.mDisplayContent;
+        if (displayContent == null) {
+            return false;
+        }
+        return displayContent.mDisplayRotationCompatPolicy != null
+                && displayContent.mDisplayRotationCompatPolicy
+                        .isTreatmentEnabledForActivity(mActivityRecord);
+    }
+
     private boolean isCompatChangeEnabled(long overrideChangeId) {
         return mActivityRecord.info.isChangeEnabled(overrideChangeId);
     }
@@ -834,6 +841,12 @@
         }
     }
 
+    boolean isFromDoubleTap() {
+        final boolean isFromDoubleTap = mDoubleTapEvent;
+        mDoubleTapEvent = false;
+        return isFromDoubleTap;
+    }
+
     SurfaceControl getLetterboxParentSurface() {
         if (mActivityRecord.isInLetterboxAnimation()) {
             return mActivityRecord.getTask().getSurfaceControl();
@@ -904,13 +917,42 @@
     }
 
     float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
-        // Don't resize to split screen size when half folded if letterbox position is centered
-        return isDisplayFullScreenAndSeparatingHinge()
-                    && getHorizontalPositionMultiplier(parentConfiguration) != 0.5f
-                        ? getSplitScreenAspectRatio()
-                        : mActivityRecord.shouldCreateCompatDisplayInsets()
-                            ? getDefaultMinAspectRatioForUnresizableApps()
-                            : getDefaultMinAspectRatio();
+        return shouldUseSplitScreenAspectRatio(parentConfiguration)
+                ? getSplitScreenAspectRatio()
+                : mActivityRecord.shouldCreateCompatDisplayInsets()
+                        ? getDefaultMinAspectRatioForUnresizableApps()
+                        : getDefaultMinAspectRatio();
+    }
+
+    void recomputeConfigurationForCameraCompatIfNeeded() {
+        if (isOverrideOrientationOnlyForCameraEnabled()
+                || isCameraCompatSplitScreenAspectRatioAllowed()) {
+            mActivityRecord.recomputeConfiguration();
+        }
+    }
+
+    /**
+     * Whether we use split screen aspect ratio for the activity when camera compat treatment
+     * is active because the corresponding config is enabled and activity supports resizing.
+     */
+    private boolean isCameraCompatSplitScreenAspectRatioAllowed() {
+        return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
+                && !mActivityRecord.shouldCreateCompatDisplayInsets();
+    }
+
+    private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
+        final boolean isBookMode = isDisplayFullScreenAndInPosture(
+                DeviceStateController.DeviceState.HALF_FOLDED,
+                /* isTabletop */ false);
+        final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier(
+                parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
+        final boolean isTabletopMode = isDisplayFullScreenAndInPosture(
+                DeviceStateController.DeviceState.HALF_FOLDED,
+                /* isTabletop */ true);
+        // Don't resize to split screen size when in book mode if letterbox position is centered
+        return ((isBookMode && isNotCenteredHorizontally) || isTabletopMode)
+                    || isCameraCompatSplitScreenAspectRatioAllowed()
+                        && isCameraCompatTreatmentActive();
     }
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -970,7 +1012,7 @@
 
     @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability() {
-        final boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge();
+        final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
         return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(
                 isInFullScreenBookMode);
     }
@@ -1011,7 +1053,7 @@
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
         }
-
+        mDoubleTapEvent = true;
         // TODO(197549949): Add animation for transition.
         mActivityRecord.recomputeConfiguration();
     }
@@ -1050,7 +1092,7 @@
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
         }
-
+        mDoubleTapEvent = true;
         // TODO(197549949): Add animation for transition.
         mActivityRecord.recomputeConfiguration();
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index aed876d..f5c44d9 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3485,6 +3485,7 @@
         info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
         info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
         info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET;
+        info.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap();
         if (info.isLetterboxDoubleTapEnabled) {
             info.topActivityLetterboxWidth = top.getBounds().width();
             info.topActivityLetterboxHeight = top.getBounds().height();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ccce558..39165dd 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -552,6 +552,16 @@
     // everything else on screen). Otherwise, it will be put under always-on-top stacks.
     final boolean mAssistantOnTopOfDream;
 
+    /**
+     * If true, don't relaunch the activity upon receiving a configuration change to transition to
+     * or from the {@link UI_MODE_TYPE_DESK} uiMode, which is sent when docking. The configuration
+     * change will still be sent regardless, only the relaunch is skipped. Apps with desk resources
+     * are exempt from this and will behave like normal, since they may expect the relaunch upon the
+     * desk uiMode change.
+     */
+    @VisibleForTesting
+    boolean mSkipActivityRelaunchWhenDocking;
+
     final boolean mLimitedAlphaCompositing;
     final int mMaxUiWidth;
 
@@ -1226,6 +1236,8 @@
                 com.android.internal.R.bool.config_perDisplayFocusEnabled);
         mAssistantOnTopOfDream = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_assistantOnTopOfDream);
+        mSkipActivityRelaunchWhenDocking = context.getResources()
+                .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking);
 
         mLetterboxConfiguration = new LetterboxConfiguration(
                 // Using SysUI context to have access to Material colors extracted from Wallpaper.
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
index dea31d7..3ad24de 100644
--- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -17,12 +17,17 @@
 
 import com.android.server.audio.SpatializerHelper.SADeviceState;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
+import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
 import android.media.AudioSystem;
 import android.util.Log;
 
@@ -36,6 +41,7 @@
 import org.mockito.Mock;
 import org.mockito.Spy;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @MediumTest
@@ -49,16 +55,35 @@
 
     @Mock private AudioService mMockAudioService;
     @Spy private AudioSystemAdapter mSpyAudioSystem;
+    @Mock private AudioSystemAdapter mMockAudioSystem;
 
     @Before
     public void setUp() throws Exception {
         mMockAudioService = mock(AudioService.class);
-        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+    }
 
-        mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem,
+    /**
+     * Initializes mSpatHelper, the SpatizerHelper instance under test, to use the mock or spy
+     * AudioSystemAdapter
+     * @param useSpyAudioSystem true to use the spy adapter, mSpyAudioSystem, or false to use
+     *                          the mock adapter, mMockAudioSystem.
+     */
+    private void setUpSpatHelper(boolean useSpyAudioSystem) {
+        final AudioSystemAdapter asAdapter;
+        if (useSpyAudioSystem) {
+            mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+            asAdapter = mSpyAudioSystem;
+            mMockAudioSystem = null;
+        } else {
+            mSpyAudioSystem = null;
+            mMockAudioSystem = mock(NoOpAudioSystemAdapter.class);
+            asAdapter = mMockAudioSystem;
+        }
+        mSpatHelper = new SpatializerHelper(mMockAudioService, asAdapter,
                 true /*binauralEnabledDefault*/,
                 true /*transauralEnabledDefault*/,
                 false /*headTrackingEnabledDefault*/);
+
     }
 
     /**
@@ -68,6 +93,7 @@
      */
     @Test
     public void testSADeviceStateNullAddressCtor() throws Exception {
+        setUpSpatHelper(true /*useSpyAudioSystem*/);
         try {
             SADeviceState devState = new SADeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
             devState = new SADeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, null);
@@ -78,6 +104,7 @@
     @Test
     public void testSADeviceStateStringSerialization() throws Exception {
         Log.i(TAG, "starting testSADeviceStateStringSerialization");
+        setUpSpatHelper(true /*useSpyAudioSystem*/);
         final SADeviceState devState = new SADeviceState(
                 AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla");
         devState.mHasHeadTracker = false;
@@ -93,6 +120,7 @@
     @Test
     public void testSADeviceSettings() throws Exception {
         Log.i(TAG, "starting testSADeviceSettings");
+        setUpSpatHelper(true /*useSpyAudioSystem*/);
         final AudioDeviceAttributes dev1 =
                 new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, "");
         final AudioDeviceAttributes dev2 =
@@ -143,4 +171,34 @@
         Log.i(TAG, "device settingsRestored: " + settingsRestored);
         Assert.assertEquals(settings, settingsRestored);
     }
+
+    /**
+     * Test that null devices for routing do not break canBeSpatialized
+     * @throws Exception
+     */
+    @Test
+    public void testNoRoutingCanBeSpatialized() throws Exception {
+        Log.i(TAG, "Starting testNoRoutingCanBeSpatialized");
+        setUpSpatHelper(false /*useSpyAudioSystem*/);
+        mSpatHelper.forceStateForTest(SpatializerHelper.STATE_ENABLED_AVAILABLE);
+
+        final ArrayList<AudioDeviceAttributes> emptyList = new ArrayList<>(0);
+        final ArrayList<AudioDeviceAttributes> listWithNull = new ArrayList<>(1);
+        listWithNull.add(null);
+        final AudioAttributes media = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA).build();
+        final AudioFormat spatialFormat = new AudioFormat.Builder()
+                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1).build();
+
+        when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean()))
+                .thenReturn(emptyList);
+        Assert.assertFalse("can be spatialized on empty routing",
+                mSpatHelper.canBeSpatialized(media, spatialFormat));
+
+        when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean()))
+                .thenReturn(listWithNull);
+        Assert.assertFalse("can be spatialized on null routing",
+                mSpatHelper.canBeSpatialized(media, spatialFormat));
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 0300eb0..2cc752c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -45,6 +45,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.os.Process.NOBODY_UID;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -490,6 +491,62 @@
     }
 
     @Test
+    public void testDeskModeChange_doesNotRelaunch() throws RemoteException {
+        mWm.mSkipActivityRelaunchWhenDocking = true;
+
+        final ActivityRecord activity = createActivityWithTask();
+        // The activity will already be relaunching out of the gate, finish the relaunch so we can
+        // test properly.
+        activity.finishRelaunching();
+        // Clear out any calls to scheduleTransaction from launching the activity.
+        reset(mAtm.getLifecycleManager());
+
+        final Task task = activity.getTask();
+        activity.setState(RESUMED, "Testing");
+
+        // Send a desk UI mode config update.
+        final Configuration newConfig = new Configuration(task.getConfiguration());
+        newConfig.uiMode |= UI_MODE_TYPE_DESK;
+        task.onRequestedOverrideConfigurationChanged(newConfig);
+        ensureActivityConfiguration(activity);
+
+        // The activity shouldn't start relaunching since it doesn't have any desk resources.
+        assertFalse(activity.isRelaunching());
+
+        // The configuration change is still sent to the activity, even if it doesn't relaunch.
+        final ActivityConfigurationChangeItem expected =
+                ActivityConfigurationChangeItem.obtain(newConfig);
+        verify(mAtm.getLifecycleManager()).scheduleTransaction(
+                eq(activity.app.getThread()), eq(activity.token), eq(expected));
+    }
+
+    @Test
+    public void testDeskModeChange_relaunchesWithDeskResources() {
+        mWm.mSkipActivityRelaunchWhenDocking = true;
+
+        final ActivityRecord activity = createActivityWithTask();
+        // The activity will already be relaunching out of the gate, finish the relaunch so we can
+        // test properly.
+        activity.finishRelaunching();
+
+        // Activities with desk resources should get relaunched when a UI_MODE_TYPE_DESK change
+        // comes in.
+        doReturn(true).when(activity).hasDeskResources();
+
+        final Task task = activity.getTask();
+        activity.setState(RESUMED, "Testing");
+
+        // Send a desk UI mode config update.
+        final Configuration newConfig = new Configuration(task.getConfiguration());
+        newConfig.uiMode |= UI_MODE_TYPE_DESK;
+        task.onRequestedOverrideConfigurationChanged(newConfig);
+        ensureActivityConfiguration(activity);
+
+        // The activity will relaunch since it has desk resources.
+        assertTrue(activity.isRelaunching());
+    }
+
+    @Test
     public void testSetRequestedOrientationUpdatesConfiguration() throws Exception {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 23a3d52..8f31641 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -820,6 +820,33 @@
         assertTrue(mController.shouldSendFakeFocus());
     }
 
+    @Test
+    public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() {
+        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(anyBoolean());
+        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+                .isCameraCompatSplitScreenAspectRatioEnabled();
+        doReturn(false).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+        doReturn(1.5f).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getFixedOrientationLetterboxAspectRatio();
+
+        // Recreate DisplayContent with DisplayRotationCompatPolicy
+        mActivity = setUpActivityWithComponent();
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertEquals(mController.getFixedOrientationLetterboxAspectRatio(
+                mActivity.getParent().getConfiguration()), 1.5f, /* delta */ 0.01);
+
+        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+                .isTreatmentEnabledForActivity(eq(mActivity));
+
+        assertEquals(mController.getFixedOrientationLetterboxAspectRatio(
+                mActivity.getParent().getConfiguration()), mController.getSplitScreenAspectRatio(),
+                /* delta */  0.01);
+    }
+
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
         Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
                  /* className */ "");
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 4a3f46a..e279145 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -58,6 +58,7 @@
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityRecord.State.STOPPED;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -3771,6 +3772,27 @@
     }
 
     @Test
+    public void testGetFixedOrientationLetterboxAspectRatio_tabletop_centered() {
+        // Set up a display in portrait with a fixed-orientation LANDSCAPE app
+        setUpDisplaySizeWithApp(1400, 2800);
+        mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+                LETTERBOX_POSITION_MULTIPLIER_CENTER);
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+                1.0f /*letterboxVerticalPositionMultiplier*/);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+        Configuration parentConfig = mActivity.getParent().getConfiguration();
+
+        float actual = mActivity.mLetterboxUiController
+                .getFixedOrientationLetterboxAspectRatio(parentConfig);
+        float expected = mActivity.mLetterboxUiController.getSplitScreenAspectRatio();
+
+        assertEquals(expected, actual, 0.01);
+    }
+
+    @Test
     public void testUpdateResolvedBoundsHorizontalPosition_bookModeEnabled() {
         // Set up a display in landscape with a fixed-orientation PORTRAIT app
         setUpDisplaySizeWithApp(2800, 1400);