Merge "Move up user verifications in startInputOrWindowGainedFocus" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 98b62b3..0ee7ace 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -36,7 +36,9 @@
     ":android.location.flags-aconfig-java{.generated_srcjars}",
     ":android.media.tv.flags-aconfig-java{.generated_srcjars}",
     ":android.multiuser.flags-aconfig-java{.generated_srcjars}",
+    ":android.net.platform.flags-aconfig-java{.generated_srcjars}",
     ":android.net.vcn.flags-aconfig-java{.generated_srcjars}",
+    ":android.net.wifi.flags-aconfig-java{.generated_srcjars}",
     ":android.nfc.flags-aconfig-java{.generated_srcjars}",
     ":android.os.flags-aconfig-java{.generated_srcjars}",
     ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}",
@@ -60,13 +62,13 @@
     ":android.webkit.flags-aconfig-java{.generated_srcjars}",
     ":android.widget.flags-aconfig-java{.generated_srcjars}",
     ":audio-framework-aconfig",
+    ":backup_flags_lib{.generated_srcjars}",
     ":camera_platform_flags_core_java_lib{.generated_srcjars}",
     ":com.android.hardware.input-aconfig-java{.generated_srcjars}",
     ":com.android.input.flags-aconfig-java{.generated_srcjars}",
     ":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}",
     ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}",
     ":com.android.media.flags.editing-aconfig-java{.generated_srcjars}",
-    ":com.android.net.flags-aconfig-java{.generated_srcjars}",
     ":com.android.net.thread.flags-aconfig-java{.generated_srcjars}",
     ":com.android.server.flags.services-aconfig-java{.generated_srcjars}",
     ":com.android.text.flags-aconfig-java{.generated_srcjars}",
@@ -108,7 +110,9 @@
         "android.media.midi-aconfig",
         "android.media.tv.flags-aconfig",
         "android.multiuser.flags-aconfig",
+        "android.net.platform.flags-aconfig",
         "android.net.vcn.flags-aconfig",
+        "android.net.wifi.flags-aconfig",
         "android.nfc.flags-aconfig",
         "android.os.flags-aconfig",
         "android.os.vibrator.flags-aconfig",
@@ -136,7 +140,6 @@
         "com.android.hardware.input.input-aconfig",
         "com.android.input.flags-aconfig",
         "com.android.media.flags.bettertogether-aconfig",
-        "com.android.net.flags-aconfig",
         "com.android.net.thread.flags-aconfig",
         "com.android.server.flags.services-aconfig",
         "com.android.text.flags-aconfig",
@@ -775,9 +778,10 @@
 
 // Networking
 aconfig_declarations {
-    name: "com.android.net.flags-aconfig",
-    package: "com.android.net.flags",
+    name: "android.net.platform.flags-aconfig",
+    package: "android.net.platform.flags",
     srcs: ["core/java/android/net/flags.aconfig"],
+    visibility: [":__subpackages__"],
 }
 
 // Thread network
@@ -788,9 +792,10 @@
 }
 
 java_aconfig_library {
-    name: "com.android.net.flags-aconfig-java",
-    aconfig_declarations: "com.android.net.flags-aconfig",
+    name: "android.net.platform.flags-aconfig-java",
+    aconfig_declarations: "android.net.platform.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    visibility: [":__subpackages__"],
 }
 
 java_aconfig_library {
@@ -1092,4 +1097,29 @@
     name: "android.crashrecovery.flags-aconfig-java",
     aconfig_declarations: "android.crashrecovery.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
-}
\ No newline at end of file
+}
+
+// Backup
+java_aconfig_library {
+    name: "backup_flags_lib",
+    aconfig_declarations: "backup_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// Wifi
+aconfig_declarations {
+    name: "android.net.wifi.flags-aconfig",
+    package: "android.net.wifi.flags",
+    srcs: ["wifi/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.net.wifi.flags-aconfig-java",
+    aconfig_declarations: "android.net.wifi.flags-aconfig",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.wifi",
+    ],
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/OWNERS b/OWNERS
index 733157f..935b768 100644
--- a/OWNERS
+++ b/OWNERS
@@ -39,3 +39,5 @@
 per-file *Ravenwood* = file:ravenwood/OWNERS
 
 per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS
+
+per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
\ No newline at end of file
diff --git a/PACKAGE_MANAGER_OWNERS b/PACKAGE_MANAGER_OWNERS
index e4549b4..eb5842b 100644
--- a/PACKAGE_MANAGER_OWNERS
+++ b/PACKAGE_MANAGER_OWNERS
@@ -1,3 +1,3 @@
-chiuwinson@google.com
+alexbuy@google.com
 patb@google.com
 schfan@google.com
\ No newline at end of file
diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
index aadbc23..add0a08 100644
--- a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
+++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
@@ -16,8 +16,6 @@
 
 package android.input
 
-import android.content.Context
-import android.content.res.Resources
 import android.os.SystemProperties
 import android.perftests.utils.PerfStatusReporter
 import android.view.InputDevice
@@ -38,8 +36,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
 
 import java.time.Duration
 
@@ -68,18 +64,6 @@
                 InputDevice.SOURCE_STYLUS, /*flags=*/0)
 }
 
-private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context {
-    val context = mock(Context::class.java)
-    val resources: Resources = mock(Resources::class.java)
-    `when`(context.getResources()).thenReturn(resources)
-    `when`(resources.getInteger(
-            com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn(
-                offset.toNanos().toInt())
-    `when`(resources.getBoolean(
-            com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction)
-    return context
-}
-
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class MotionPredictorBenchmark {
@@ -115,7 +99,7 @@
         var eventPosition = 0f
         val positionInterval = 10f
 
-        val predictor = MotionPredictor(getPredictionContext(offset, /*enablePrediction=*/true))
+        val predictor = MotionPredictor(/*isPredictionEnabled=*/true, offset.toNanos().toInt())
         // ACTION_DOWN t=0 x=0 y=0
         predictor.record(getStylusMotionEvent(
             eventTime, ACTION_DOWN, /*x=*/eventPosition, /*y=*/eventPosition))
@@ -141,12 +125,11 @@
      */
     @Test
     fun timeCreatePredictor() {
-        val context = getPredictionContext(
-                /*offset=*/Duration.ofMillis(20), /*enablePrediction=*/true)
+        val offsetNanos = Duration.ofMillis(20).toNanos().toInt()
 
         val state = perfStatusReporter.getBenchmarkState()
         while (state.keepRunning()) {
-            MotionPredictor(context)
+            MotionPredictor(/*isPredictionEnabled=*/true, offsetNanos)
         }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
index e08200b..33f6899 100644
--- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
@@ -743,7 +743,8 @@
 
     private final class AppOpsWatcher extends IAppOpsCallback.Stub {
         @Override
-        public void opChanged(int op, int uid, String packageName) throws RemoteException {
+        public void opChanged(int op, int uid, String packageName,
+                String persistentDeviceId) throws RemoteException {
             boolean restricted = false;
             try {
                 restricted = mAppOpsService.checkOperation(TARGET_OP,
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index abf8008..39de0af 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2026,8 +2026,8 @@
                 iAppOpsService.startWatchingMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, null,
                         new IAppOpsCallback.Stub() {
                             @Override
-                            public void opChanged(int op, int uid, String packageName)
-                                    throws RemoteException {
+                            public void opChanged(int op, int uid, String packageName,
+                                    String persistentDeviceId) throws RemoteException {
                                 final int userId = UserHandle.getUserId(uid);
                                 if (op != AppOpsManager.OP_SCHEDULE_EXACT_ALARM
                                         || !isExactAlarmChangeEnabled(packageName, userId)) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 357e1396..6635484 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -227,7 +227,7 @@
 
     private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() {
         @Override
-        public void opChanged(int op, int uid, String packageName) {
+        public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
             boolean restricted = false;
             try {
                 restricted = mAppOpsService.checkOperation(
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index e589c21..19bc716 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -671,7 +671,8 @@
                         /*packageName=*/ null,
                         new IAppOpsCallback.Stub() {
                             @Override
-                            public void opChanged(int op, int uid, String packageName) {
+                            public void opChanged(int op, int uid, String packageName,
+                                    String persistentDeviceId) {
                                 final int userId = UserHandle.getUserId(uid);
                                 synchronized (mSystemExemptionAppOpMode) {
                                     mSystemExemptionAppOpMode.delete(uid);
diff --git a/core/TEST_MAPPING b/core/TEST_MAPPING
index 24ba5c4..f1e4d0ee 100644
--- a/core/TEST_MAPPING
+++ b/core/TEST_MAPPING
@@ -23,7 +23,7 @@
   ],
   "postsubmit": [
       {
-        "name": "ContactKeysManagerTest",
+        "name": "CtsContactKeysManagerTestCases",
         "options": [
           {
             "include-filter": "android.provider.cts.contactkeys."
diff --git a/core/api/current.txt b/core/api/current.txt
index cb8db9e..cf5a261 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -147,6 +147,7 @@
     field public static final String MANAGE_DEVICE_POLICY_CAMERA = "android.permission.MANAGE_DEVICE_POLICY_CAMERA";
     field public static final String MANAGE_DEVICE_POLICY_CERTIFICATES = "android.permission.MANAGE_DEVICE_POLICY_CERTIFICATES";
     field public static final String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = "android.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE";
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = "android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION";
     field public static final String MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES = "android.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES";
     field public static final String MANAGE_DEVICE_POLICY_DEFAULT_SMS = "android.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS";
     field public static final String MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS = "android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS";
@@ -5094,10 +5095,12 @@
 
   public static interface AppOpsManager.OnOpActiveChangedListener {
     method public void onOpActiveChanged(@NonNull String, int, @NonNull String, boolean);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, int, boolean, int, int);
   }
 
   public static interface AppOpsManager.OnOpChangedListener {
     method public void onOpChanged(String, String);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpChanged(@NonNull String, @NonNull String, int, @NonNull String);
   }
 
   public abstract static class AppOpsManager.OnOpNotedCallback {
@@ -6352,6 +6355,7 @@
     field public static final String CATEGORY_STOPWATCH = "stopwatch";
     field public static final String CATEGORY_SYSTEM = "sys";
     field public static final String CATEGORY_TRANSPORT = "transport";
+    field @FlaggedApi("android.app.category_voicemail") public static final String CATEGORY_VOICEMAIL = "voicemail";
     field public static final String CATEGORY_WORKOUT = "workout";
     field @ColorInt public static final int COLOR_DEFAULT = 0; // 0x0
     field @NonNull public static final android.os.Parcelable.Creator<android.app.Notification> CREATOR;
@@ -8194,6 +8198,9 @@
     field public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD";
     field public static final String ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION";
     field public static final String ACTION_SYSTEM_UPDATE_POLICY_CHANGED = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_DISABLED = 1; // 0x1
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_ENABLED = 2; // 0x2
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; // 0x0
     field public static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions";
     field public static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall";
     field public static final String DELEGATION_CERT_INSTALL = "delegation-cert-install";
@@ -12387,6 +12394,7 @@
     method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler);
     method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback);
     method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle);
+    method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean);
     method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle);
     method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
     method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
@@ -15566,6 +15574,7 @@
     method public boolean clipRect(float, float, float, float);
     method public boolean clipRect(int, int, int, int);
     method public void concat(@Nullable android.graphics.Matrix);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat44(@Nullable android.graphics.Matrix44);
     method public void disableZ();
     method public void drawARGB(int, int, int, int);
     method public void drawArc(@NonNull android.graphics.RectF, float, float, boolean, @NonNull android.graphics.Paint);
@@ -16214,6 +16223,24 @@
     enum_constant public static final android.graphics.Matrix.ScaleToFit START;
   }
 
+  @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public class Matrix44 {
+    ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44();
+    ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(@NonNull android.graphics.Matrix);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 concat(@NonNull android.graphics.Matrix44);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(int, int);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void getValues(@NonNull float[]);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean invert();
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean isIdentity();
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public float[] map(float, float, float, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void map(float, float, float, float, @NonNull float[]);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void reset();
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 rotate(float, float, float, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 scale(float, float, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(int, int, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void setValues(@NonNull float[]);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 translate(float, float, float);
+  }
+
   public class Mesh {
     ctor public Mesh(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull android.graphics.RectF);
     ctor public Mesh(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull java.nio.ShortBuffer, @NonNull android.graphics.RectF);
@@ -21167,6 +21194,7 @@
     method public int getStreamMinVolume(int);
     method public int getStreamVolume(int);
     method public float getStreamVolumeDb(int, int, int);
+    method @FlaggedApi("android.media.audio.supported_device_types_api") @NonNull public java.util.Set<java.lang.Integer> getSupportedDeviceTypes(int);
     method @NonNull public java.util.List<android.media.AudioMixerAttributes> getSupportedMixerAttributes(@NonNull android.media.AudioDeviceInfo);
     method @Deprecated public int getVibrateSetting(int);
     method public int getVolumeGroupIdForAttributes(@NonNull android.media.AudioAttributes);
@@ -25785,6 +25813,7 @@
     field public static final int FINAL_STATE_CANCELED = 2; // 0x2
     field public static final int FINAL_STATE_ERROR = 3; // 0x3
     field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
   }
 
   @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
@@ -25792,7 +25821,7 @@
     method @NonNull public android.media.metrics.EditingEndedEvent build();
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
-    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
   }
 
   public final class EditingSession implements java.lang.AutoCloseable {
@@ -33140,7 +33169,7 @@
     method public static long getStartRequestedElapsedRealtime();
     method public static long getStartRequestedUptimeMillis();
     method public static long getStartUptimeMillis();
-    method public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
+    method @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
     method public static final int getUidForName(String);
     method public static final boolean is64Bit();
     method public static boolean isApplicationUid(int);
@@ -33155,8 +33184,8 @@
     method public static final int myUid();
     method public static android.os.UserHandle myUserHandle();
     method public static final void sendSignal(int, int);
-    method public static final void setThreadPriority(int, int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
-    method public static final void setThreadPriority(int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+    method public static final void setThreadPriority(int, @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+    method public static final void setThreadPriority(@IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
     method @Deprecated public static final boolean supportsProcesses();
     field public static final int BLUETOOTH_UID = 1002; // 0x3ea
     field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
@@ -50644,6 +50673,7 @@
     field public static final int KEYCODE_DVR = 173; // 0xad
     field public static final int KEYCODE_E = 33; // 0x21
     field public static final int KEYCODE_EISU = 212; // 0xd4
+    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d
     field public static final int KEYCODE_ENDCALL = 6; // 0x6
     field public static final int KEYCODE_ENTER = 66; // 0x42
     field public static final int KEYCODE_ENVELOPE = 65; // 0x41
@@ -50776,6 +50806,7 @@
     field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
     field public static final int KEYCODE_RO = 217; // 0xd9
     field public static final int KEYCODE_S = 47; // 0x2f
+    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e
     field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74
     field public static final int KEYCODE_SEARCH = 84; // 0x54
     field public static final int KEYCODE_SEMICOLON = 74; // 0x4a
@@ -59537,7 +59568,6 @@
   }
 
   @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions {
-    method @FlaggedApi("android.appwidget.flags.draw_data_parcel") public void appendInstructions(@NonNull byte[]);
   }
 
   @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions.Builder {
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 24b9233..55ed1f5 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -66,6 +66,11 @@
     method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean);
   }
 
+  public final class SystemServiceRegistry {
+    method @FlaggedApi("android.webkit.update_service_ipc_wrapper") @Nullable public static Object getSystemServiceWithNoContext(@NonNull String);
+    method @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static <TServiceClass> void registerForeverStaticService(@NonNull String, @NonNull Class<TServiceClass>, @NonNull android.app.SystemServiceRegistry.StaticServiceProducerWithBinder<TServiceClass>);
+  }
+
 }
 
 package android.app.admin {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 94cde98..86f2b628 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -561,7 +561,7 @@
 
   public class ActivityManager {
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int);
-    method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @Nullable int[]);
+    method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @NonNull int[]);
     method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String);
     method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int);
     method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser();
@@ -822,6 +822,7 @@
 
   public static interface AppOpsManager.OnOpNotedListener {
     method public void onOpNoted(@NonNull String, int, @NonNull String, @Nullable String, int, int);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpNoted(@NonNull String, int, @NonNull String, @Nullable String, int, int, int);
   }
 
   public static final class AppOpsManager.OpEntry implements android.os.Parcelable {
@@ -3208,8 +3209,14 @@
 
 package android.companion.virtual {
 
+  public final class VirtualDevice implements android.os.Parcelable {
+    method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomAudioInputSupport();
+    method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomCameraSupport();
+  }
+
   public final class VirtualDeviceManager {
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+    method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @NonNull public java.util.Set<java.lang.String> getAllPersistentDeviceIds();
     method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String);
     field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
     field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
@@ -6165,6 +6172,7 @@
     method public boolean containsKey(String);
     method public int describeContents();
     method @Deprecated public android.graphics.Bitmap getBitmap(String);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") public int getBitmapId(@NonNull String);
     method public android.hardware.radio.RadioMetadata.Clock getClock(String);
     method public int getInt(String);
     method public String getString(String);
@@ -6228,6 +6236,7 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
     method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.graphics.Bitmap getMetadataImage(int);
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
@@ -9655,6 +9664,7 @@
   public final class DeviceWiphyCapabilities implements android.os.Parcelable {
     ctor public DeviceWiphyCapabilities();
     method public int describeContents();
+    method @FlaggedApi("android.net.wifi.flags.get_device_cross_akm_roaming_support") public int getMaxNumberAkms();
     method public int getMaxNumberRxSpatialStreams();
     method public int getMaxNumberTxSpatialStreams();
     method public boolean isChannelWidthSupported(int);
@@ -14846,11 +14856,11 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean);
     method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean);
-    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1fa2b5c..77add41 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2654,7 +2654,6 @@
     method @NonNull public static String convert(@NonNull java.util.UUID);
     method @Nullable public String getCloudMediaProvider();
     method public boolean isAppIoBlocked(@NonNull java.util.UUID, int, int, int);
-    method public static boolean isUserKeyUnlocked(int);
     field public static final String CACHE_RESERVE_PERCENT_HIGH_KEY = "cache_reserve_percent_high";
     field public static final String CACHE_RESERVE_PERCENT_LOW_KEY = "cache_reserve_percent_low";
     field public static final String STORAGE_THRESHOLD_PERCENT_HIGH_KEY = "storage_threshold_percent_high";
@@ -3617,7 +3616,7 @@
     method public final int getDisplayId();
     method public final void setDisplayId(int);
     field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
-    field public static final int LAST_KEYCODE = 316; // 0x13c
+    field public static final int LAST_KEYCODE = 318; // 0x13e
   }
 
   public final class KeyboardShortcutGroup implements android.os.Parcelable {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 9d20f3c..6285eb3 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -107,6 +107,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -4440,8 +4441,7 @@
      * is used here, you will receive a call each time a uids importance transitions between
      * being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and
      * > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}.
-     * @param uids The UIDs that this listener is interested with. A {@code null} value means
-     * all UIDs will be monitored by this listener, this will be equivalent to the
+     * @param uids The UIDs that this listener is interested with.
      * {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case.
      *
      * <p>Calling this API with the same instance of {@code listener} without
@@ -4456,7 +4456,9 @@
     @SuppressLint("SamShouldBeLast")
     @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
     public void addOnUidImportanceListener(@NonNull OnUidImportanceListener listener,
-            @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) {
+            @RunningAppProcessInfo.Importance int importanceCutpoint, @NonNull int[] uids) {
+        Objects.requireNonNull(listener);
+        Objects.requireNonNull(uids);
         addOnUidImportanceListenerInternal(listener, importanceCutpoint, uids);
     }
 
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 00c4b0f..7907059 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -36,6 +36,7 @@
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.app.usage.UsageStatsManager;
+import android.companion.virtual.VirtualDeviceManager;
 import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -1555,9 +1556,24 @@
      */
     public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS;
 
+    /**
+     * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+     *
+     * @hide
+     */
+    public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY;
+
+    /**
+     * Whether the app has enabled compatibility support for unarchival.
+     *
+     * @hide
+     */
+    public static final int OP_UNARCHIVAL_CONFIRMATION =
+            AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 145;
+    public static final int _NUM_OP = 147;
 
     /**
      * All app ops represented as strings.
@@ -1708,6 +1724,8 @@
             OPSTR_RESERVED_FOR_TESTING,
             OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
             OPSTR_RUN_BACKUP_JOBS,
+            OPSTR_ARCHIVE_ICON_OVERLAY,
+            OPSTR_UNARCHIVAL_CONFIRMATION,
     })
     public @interface AppOpString {}
 
@@ -2048,6 +2066,20 @@
     public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
 
     /**
+     * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+     *
+     * @hide
+     */
+    public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay";
+
+    /**
+     * Whether the app has enabled compatibility support for unarchival.
+     *
+     * @hide
+     */
+    public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support";
+
+    /**
      * AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage}
      *
      * <p>MediaProvider is the only component (outside of system server) that should care about this
@@ -2520,6 +2552,8 @@
             OP_MEDIA_ROUTING_CONTROL,
             OP_READ_SYSTEM_GRAMMATICAL_GENDER,
             OP_RUN_BACKUP_JOBS,
+            OP_ARCHIVE_ICON_OVERLAY,
+            OP_UNARCHIVAL_CONFIRMATION,
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2979,6 +3013,12 @@
                 .build(),
         new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS")
                 .setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(),
+        new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY,
+                "ARCHIVE_ICON_OVERLAY")
+                .setDefaultMode(MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
+                "UNARCHIVAL_CONFIRMATION")
+                .setDefaultMode(MODE_ALLOWED).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
@@ -3113,7 +3153,7 @@
 
     /**
      * Retrieve the permission associated with an operation, or null if there is not one.
-     *
+
      * @param op The operation name.
      *
      * @hide
@@ -7307,6 +7347,12 @@
 
          * The default impl is to fallback onto {@link #onOpChanged(String, String)
          *
+         * <p> Implement this method and not
+         * {@link #onOpChanged(String, String, int, String)} if
+         * callbacks are
+         * required only on op state changes for the default device
+         * {@link VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT}.
+         *
          * @param op The Op that changed.
          * @param packageName Package of the app whose Op changed.
          * @param userId User Space of the app whose Op changed.
@@ -7315,6 +7361,31 @@
         default void onOpChanged(@NonNull String op, @NonNull String packageName,  int userId) {
             onOpChanged(op, packageName);
         }
+
+        /**
+         * Similar to {@link #onOpChanged(String, String, int)} but includes the device for which
+         * the op mode has changed.
+         *
+         * <p> Implement this method if callbacks are required on all devices.
+         * If not implemented explicitly, the default implementation will notify for op changes
+         * on the default device {@link VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT} only.
+         *
+         * <p> If implemented, {@link #onOpChanged(String, String, int)}
+         * will not be called automatically.
+         *
+         * @param op The Op that changed.
+         * @param packageName Package of the app whose Op changed.
+         * @param userId User id of the app whose Op changed.
+         * @param persistentDeviceId persistent device id whose Op changed.
+         */
+        @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+        default void onOpChanged(@NonNull String op, @NonNull String packageName, int userId,
+                @NonNull String persistentDeviceId) {
+            if (Objects.equals(persistentDeviceId,
+                    VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+                onOpChanged(op, packageName, userId);
+            }
+        }
     }
 
     /**
@@ -7334,6 +7405,12 @@
         /**
          * Called when the active state of an app-op changes.
          *
+         * <p> Implement this method and not
+         * {@link #onOpActiveChanged(String, int, String, String, int, boolean, int, int)} if
+         * callbacks are
+         * required only on op state changes for the default device
+         * {@link Context#DEVICE_ID_DEFAULT}.
+         *
          * @param op The operation that changed.
          * @param uid The UID performing the operation.
          * @param packageName The package performing the operation.
@@ -7349,6 +7426,37 @@
                 int attributionFlags, int attributionChainId) {
             onOpActiveChanged(op, uid, packageName, active);
         }
+
+        /**
+         * Similar to {@link #onOpActiveChanged(String, int, String, String, boolean, int, int)},
+         * but also includes the virtual device id of the op is now active or inactive.
+         *
+         * <p> Implement this method if callbacks are required on all devices.
+         * If not implemented explicitly, the default implementation will notify for op state
+         * changes on the default device {@link Context#DEVICE_ID_DEFAULT} only.
+         *
+         * <p> If implemented,
+         * {@link #onOpActiveChanged(String, int, String, String, boolean, int, int)}
+         * will not be called automatically.
+         *
+         * @param op The operation that changed.
+         * @param uid The UID performing the operation.
+         * @param packageName The package performing the operation.
+         * @param attributionTag The operation's attribution tag.
+         * @param virtualDeviceId the virtual device id whose operation has changed
+         * @param active Whether the operation became active or inactive.
+         * @param attributionFlags the attribution flags for this operation.
+         * @param attributionChainId the unique id of the attribution chain this op is a part of.
+         */
+        @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+        default void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
+                @Nullable String attributionTag, int virtualDeviceId, boolean active,
+                @AttributionFlags int attributionFlags, int attributionChainId) {
+            if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+                onOpActiveChanged(op, uid, packageName, attributionTag, active, attributionFlags,
+                        attributionChainId);
+            }
+        }
     }
 
     /**
@@ -7361,6 +7469,10 @@
         /**
          * Called when an app-op is noted.
          *
+         * <p> Implement this method and not
+         * {@link #onOpNoted(String, int, String, String, int, int, int)} if callbacks are
+         * required only on op notes for the default device {@link Context#DEVICE_ID_DEFAULT}.
+         *
          * @param op The operation that was noted.
          * @param uid The UID performing the operation.
          * @param packageName The package performing the operation.
@@ -7370,6 +7482,34 @@
          */
         void onOpNoted(@NonNull String op, int uid, @NonNull String packageName,
                 @Nullable String attributionTag, @OpFlags int flags, @Mode int result);
+
+        /**
+         * Similar to {@link #onOpNoted(String, int, String, String, int, int, int)},
+         * but also includes the virtual device id of the op is now active or inactive.
+         *
+         * <p> Implement this method if callbacks are required for op notes on all devices.
+         * If not implemented explicitly, the default implementation will notify for the
+         * default device {@link Context#DEVICE_ID_DEFAULT} only.
+         *
+         * <p> If implemented, {@link #onOpNoted(String, int, String, String, int, int)}
+         * will not be called automatically.
+         *
+         * @param op The operation that was noted.
+         * @param uid The UID performing the operation.
+         * @param packageName The package performing the operation.
+         * @param attributionTag The attribution tag performing the operation.
+         * @param virtualDeviceId the device that noted the operation
+         * @param flags The flags of this op
+         * @param result The result of the note.
+         */
+        @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+        default void onOpNoted(@NonNull String op, int uid, @NonNull String packageName,
+                @Nullable String attributionTag, int virtualDeviceId, @OpFlags int flags,
+                @Mode int result) {
+            if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+                onOpNoted(op, uid, packageName, attributionTag, flags, result);
+            }
+        }
     }
 
     /**
@@ -7408,6 +7548,9 @@
     public static class OnOpChangedInternalListener implements OnOpChangedListener {
         public void onOpChanged(String op, String packageName) { }
         public void onOpChanged(int op, String packageName) { }
+        public void onOpChanged(int op, String packageName, String persistentDeviceId) {
+            onOpChanged(op, packageName);
+        }
     }
 
     /**
@@ -7418,6 +7561,8 @@
     public interface OnOpActiveChangedInternalListener extends OnOpActiveChangedListener {
         default void onOpActiveChanged(String op, int uid, String packageName, boolean active) { }
         default void onOpActiveChanged(int op, int uid, String packageName, boolean active) { }
+        default void onOpActiveChanged(int op, int uid, String packageName, int virtualDeviceId,
+                boolean active) { }
     }
 
     /**
@@ -7471,6 +7616,12 @@
         /**
          * Called when an op was started.
          *
+         * <p> Implement this method and not
+         * {@link #onOpStarted(int, int, String, String, int, int, int, int, int, int)} if
+         * callbacks are
+         * required only on op starts for the default device
+         * {@link Context#DEVICE_ID_DEFAULT}.
+         *
          * Note: This is only for op starts. It is not called when an op is noted or stopped.
          * By default, unless this method is overridden, no code will be executed for resume
          * events.
@@ -7491,6 +7642,37 @@
                 onOpStarted(op, uid, packageName, attributionTag, flags, result);
             }
         }
+
+        /**
+         * Similar to {@link #onOpStarted(int, int, String, String, int, int)},
+         * but also includes the virtual device id that started the op.
+         *
+         * <p> Implement this method if callbacks are required on all devices.
+         * If not implemented explicitly, the default implementation will notify for op starts on
+         * the default device {@link Context#DEVICE_ID_DEFAULT} only.
+         *
+         * <p> If implemented, {@link #onOpStarted(int, int, String, String, int, int)}
+         * will not be called automatically.
+         *
+         * @param op The op code.
+         * @param uid The UID performing the operation.
+         * @param packageName The package performing the operation.
+         * @param attributionTag The attribution tag performing the operation.
+         * @param virtualDeviceId the device that started the operation
+         * @param flags The flags of this op.
+         * @param result The result of the start.
+         * @param startType The start type of this start event. Either failed, resumed, or started.
+         * @param attributionFlags The location of this started op in an attribution chain.
+         */
+        default void onOpStarted(int op, int uid, @NonNull String packageName,
+             @Nullable String attributionTag, int virtualDeviceId, @OpFlags int flags,
+             @Mode int result, @StartedType int startType,
+             @AttributionFlags int attributionFlags, int attributionChainId) {
+            if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+                onOpStarted(op, uid, packageName, attributionTag, flags, result, startType,
+                        attributionFlags, attributionChainId);
+            }
+        }
     }
 
     AppOpsManager(Context context, IAppOpsService service) {
@@ -8015,14 +8197,16 @@
             IAppOpsCallback cb = mModeWatchers.get(callback);
             if (cb == null) {
                 cb = new IAppOpsCallback.Stub() {
-                    public void opChanged(int op, int uid, String packageName) {
+                    public void opChanged(int op, int uid, String packageName,
+                            String persistentDeviceId) {
                         if (callback instanceof OnOpChangedInternalListener) {
-                            ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName);
+                            ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName,
+                                persistentDeviceId);
                         }
                         if (sAppOpInfos[op].name != null) {
 
                             callback.onOpChanged(sAppOpInfos[op].name, packageName,
-                                    UserHandle.getUserId(uid));
+                                    UserHandle.getUserId(uid), persistentDeviceId);
                         }
                     }
                 };
@@ -8103,16 +8287,17 @@
             cb = new IAppOpsActiveCallback.Stub() {
                 @Override
                 public void opActiveChanged(int op, int uid, String packageName,
-                        String attributionTag, boolean active, @AttributionFlags
-                        int attributionFlags, int attributionChainId) {
+                        String attributionTag, int virtualDeviceId, boolean active,
+                        @AttributionFlags int attributionFlags, int attributionChainId) {
                     executor.execute(() -> {
                         if (callback instanceof OnOpActiveChangedInternalListener) {
                             ((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
-                                    uid, packageName, active);
+                                    uid, packageName, virtualDeviceId, active);
                         }
                         if (sAppOpInfos[op].name != null) {
                             callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
-                                    attributionTag, active, attributionFlags, attributionChainId);
+                                    attributionTag, virtualDeviceId, active, attributionFlags,
+                                    attributionChainId);
                         }
                     });
                 }
@@ -8181,10 +8366,10 @@
              cb = new IAppOpsStartedCallback.Stub() {
                  @Override
                  public void opStarted(int op, int uid, String packageName, String attributionTag,
-                         int flags, int mode, int startType, int attributionFlags,
-                         int attributionChainId) {
-                     callback.onOpStarted(op, uid, packageName, attributionTag, flags, mode,
-                             startType, attributionFlags, attributionChainId);
+                         int virtualDeviceId, int flags, int mode, int startType,
+                         int attributionFlags, int attributionChainId) {
+                     callback.onOpStarted(op, uid, packageName, attributionTag, virtualDeviceId,
+                             flags, mode, startType, attributionFlags, attributionChainId);
                  }
              };
              mStartedWatchers.put(callback, cb);
@@ -8352,13 +8537,13 @@
             cb = new IAppOpsNotedCallback.Stub() {
                 @Override
                 public void opNoted(int op, int uid, String packageName, String attributionTag,
-                        int flags, int mode) {
+                        int virtualDeviceId, int flags, int mode) {
                     final long identity = Binder.clearCallingIdentity();
                     try {
                         executor.execute(() -> {
                             if (sAppOpInfos[op].name != null) {
                                 listener.onOpNoted(sAppOpInfos[op].name, uid, packageName,
-                                        attributionTag,
+                                        attributionTag, virtualDeviceId,
                                         flags, mode);
                             }
                         });
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 34c44f9..4f1db7d 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -4032,7 +4032,8 @@
     private Drawable getArchivedAppIcon(String packageName) {
         try {
             return new BitmapDrawable(null,
-                    mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId())));
+                    mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()),
+                            mContext.getPackageName()));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 2162e3a..68512b8 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -79,6 +79,7 @@
  * implementation is described to the system through an AndroidManifest.xml's
  * &lt;instrumentation&gt; tag.
  */
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
 public class Instrumentation {
 
     /**
@@ -132,6 +133,7 @@
     private UiAutomation mUiAutomation;
     private final Object mAnimationCompleteLock = new Object();
 
+    @android.ravenwood.annotation.RavenwoodKeep
     public Instrumentation() {
     }
 
@@ -142,6 +144,7 @@
      * reflection, but it will serve as noticeable discouragement from
      * doing such a thing.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     private void checkInstrumenting(String method) {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -151,6 +154,11 @@
         }
     }
 
+    private void checkInstrumenting$ravenwood(String method) {
+        // At the moment, Ravenwood doesn't attach a Context, but we're only ever
+        // running code as part of tests, so we continue quietly
+    }
+
     /**
      * Returns if it is being called in an instrumentation environment.
      *
@@ -2504,6 +2512,7 @@
      * Takes control of the execution of messages on the specified looper until
      * {@link TestLooperManager#release} is called.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public TestLooperManager acquireLooperManager(Looper looper) {
         checkInstrumenting("acquireLooperManager");
         return new TestLooperManager(looper);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index a81ad3c..d705eeb 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1061,6 +1061,12 @@
     public static final String CATEGORY_MISSED_CALL = "missed_call";
 
     /**
+     * Notification category: voicemail.
+     */
+    @FlaggedApi(Flags.FLAG_CATEGORY_VOICEMAIL)
+    public static final String CATEGORY_VOICEMAIL = "voicemail";
+
+    /**
      * One of the predefined notification categories (see the <code>CATEGORY_*</code> constants)
      * that best describes this Notification.  May be used by the system for ranking and filtering.
      */
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 3b5bba2..729f92a 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -111,5 +111,9 @@
 per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
 
+# Multitasking
+per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
+per-file multitasking.aconfig = file:/libs/WindowManager/Shell/OWNERS
+
 # Zygote
 per-file *Zygote* = file:/ZYGOTE_OWNERS
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 0261f0a..1ac08ac 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -179,6 +179,14 @@
     @Overridable
     public static final long BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT = 236704164L;
 
+    /**
+     * Validate options passed in as bundle.
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long PENDING_INTENT_OPTIONS_CHECK = 320664730L;
+
     /** @hide */
     @IntDef(flag = true,
             value = {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 397b63f..ba9c895 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -19,6 +19,7 @@
 import android.accounts.AccountManager;
 import android.accounts.IAccountManager;
 import android.adservices.AdServicesFrameworkInitializer;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -1617,6 +1618,19 @@
                         return new ContactKeysManager(ctx);
                     }});
 
+        // DO NOT do a flag check like this unless the flag is read-only.
+        // (because this code is executed during preload in zygote.)
+        // If the flag is mutable, the check should be inside CachedServiceFetcher.
+        if (Flags.bicClient()) {
+            registerService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE,
+                    BackgroundInstallControlManager.class,
+                    new CachedServiceFetcher<BackgroundInstallControlManager>() {
+                        @Override
+                        public BackgroundInstallControlManager createService(ContextImpl ctx) {
+                            return new BackgroundInstallControlManager(ctx);
+                        }
+                    });
+        }
         sInitializing = true;
         try {
             // Note: the following functions need to be @SystemApis, once they become mainline
@@ -1668,11 +1682,7 @@
         return new Object[sServiceCacheSize];
     }
 
-    /**
-     * Gets a system service from a given context.
-     * @hide
-     */
-    public static Object getSystemService(ContextImpl ctx, String name) {
+    private static ServiceFetcher<?> getSystemServiceFetcher(String name) {
         if (name == null) {
             return null;
         }
@@ -1683,6 +1693,18 @@
             }
             return null;
         }
+        return fetcher;
+    }
+
+    /**
+     * Gets a system service from a given context.
+     * @hide
+     */
+    public static Object getSystemService(@NonNull ContextImpl ctx, String name) {
+        final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name);
+        if (fetcher == null) {
+            return null;
+        }
 
         final Object ret = fetcher.getService(ctx);
         if (sEnableServiceNotFoundWtf && ret == null) {
@@ -1710,6 +1732,26 @@
     }
 
     /**
+     * Gets a system service which has opted-in to being fetched without a context.
+     * @hide
+     */
+    @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static @Nullable Object getSystemServiceWithNoContext(@NonNull String name) {
+        final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name);
+        if (fetcher == null) {
+            return null;
+        }
+
+        if (!fetcher.supportsFetchWithoutContext()) {
+            throw new IllegalArgumentException(
+                    "Manager cannot be fetched without a context: " + name);
+        }
+
+        return fetcher.getService(null);
+    }
+
+    /**
      * Gets the name of the system-level service that is represented by the specified class.
      * @hide
      */
@@ -1863,6 +1905,50 @@
     }
 
     /**
+     * Used by apex modules to register a "service wrapper" that is not tied to any {@link Context}
+     * and will never require a context in the future.
+     *
+     * Services registered in this way can be fetched via
+     * {@link #getSystemServiceWithNoContext(String)}, so cannot require a context in future without
+     * a breaking change.
+     *
+     * <p>This can only be called from the methods called by the static initializer of
+     * {@link SystemServiceRegistry}. (Otherwise it throws a {@link IllegalStateException}.)
+     *
+     * @param serviceName the name of the binder object, such as
+     *     {@link Context#JOB_SCHEDULER_SERVICE}.
+     * @param serviceWrapperClass the wrapper class, such as the class of
+     *     {@link android.app.job.JobScheduler}.
+     * @param serviceProducer Callback that takes the service binder object with the name
+     *     {@code serviceName} and returns an actual service wrapper instance.
+     *
+     * @hide
+     */
+    @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static <TServiceClass> void registerForeverStaticService(
+            @NonNull String serviceName, @NonNull Class<TServiceClass> serviceWrapperClass,
+            @NonNull StaticServiceProducerWithBinder<TServiceClass> serviceProducer) {
+        ensureInitializing("registerStaticService");
+        Preconditions.checkStringNotEmpty(serviceName);
+        Objects.requireNonNull(serviceWrapperClass);
+        Objects.requireNonNull(serviceProducer);
+
+        registerService(serviceName, serviceWrapperClass,
+                new StaticServiceFetcher<TServiceClass>() {
+                    @Override
+                    public TServiceClass createService() throws ServiceNotFoundException {
+                        return serviceProducer.createService(
+                                ServiceManager.getServiceOrThrow(serviceName));
+                    }
+
+                    @Override
+                    public boolean supportsFetchWithoutContext() {
+                        return true;
+                    }});
+    }
+
+    /**
      * Similar to {@link #registerStaticService(String, Class, StaticServiceProducerWithBinder)},
      * but used for a "service wrapper" that doesn't take a service binder in its constructor.
      *
@@ -1952,6 +2038,18 @@
      */
     static abstract interface ServiceFetcher<T> {
         T getService(ContextImpl ctx);
+
+        /**
+         * Should this service fetcher support being fetched via {@link #getSystemService(String)},
+         * without a Context?
+         *
+         * This means that the service cannot depend on a Context in future!
+         *
+         * @return true if this is supported for this service.
+         */
+        default boolean supportsFetchWithoutContext() {
+            return false;
+        }
     }
 
     /**
@@ -2059,6 +2157,11 @@
         }
 
         public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
+
+        // Services that explicitly use a Context can never be fetched without one.
+        public final boolean supportsFetchWithoutContext() {
+            return false;
+        }
     }
 
     /**
@@ -2083,6 +2186,13 @@
         }
 
         public abstract T createService() throws ServiceNotFoundException;
+
+        // Services that do not need a Context can potentially be fetched without one, but the
+        // default is false, so that the service can require one in future without this being a
+        // breaking change.
+        public boolean supportsFetchWithoutContext() {
+            return false;
+        }
     }
 
     /** @hide */
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5c42b0e..86d0125 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -53,6 +53,7 @@
 import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
@@ -61,6 +62,7 @@
 import android.annotation.BroadcastBehavior;
 import android.annotation.CallbackExecutor;
 import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -4092,6 +4094,29 @@
         return MTE_NOT_CONTROLLED_BY_POLICY;
     }
 
+    /** Indicates that content protection is not controlled by policy, allowing user to choose. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0;
+
+    /** Indicates that content protection is controlled and disabled by a policy. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_DISABLED = 1;
+
+    /** Indicates that content protection is controlled and enabled by a policy. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_ENABLED = 2;
+
+    /** @hide */
+    @IntDef(
+            prefix = {"CONTENT_PROTECTION_"},
+            value = {
+                CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY,
+                CONTENT_PROTECTION_DISABLED,
+                CONTENT_PROTECTION_ENABLED,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ContentProtectionPolicy {}
+
     /**
      * This object is a single place to tack on invalidation and disable calls.  All
      * binder caches in this class derive from this Config, so all can be invalidated or
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
index 029b93a..4473b95 100644
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -1,7 +1,7 @@
 package: "android.app"
 
 flag {
-     namespace: "background_install_control"
+     namespace: "preload_safety"
      name: "bic_client"
      description: "System API for background install control."
      is_fixed_read_only: true
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 6b558d0..ffbd80c 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -43,12 +43,14 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
+import com.android.server.backup.Flags;
 
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -1283,6 +1285,14 @@
                 // And bring live SharedPreferences instances up to date
                 reloadSharedPreferences();
 
+                // It's possible that onRestoreFile was overridden and that the agent did not
+                // consume all the data for this file from the pipe. We need to clear the pipe,
+                // otherwise the framework can get stuck trying to write to a full pipe or
+                // onRestoreFile could be called with the previous file's data left in the pipe.
+                if (Flags.enableClearPipeAfterRestoreFile()) {
+                    clearUnconsumedDataFromPipe(data, size);
+                }
+
                 Binder.restoreCallingIdentity(ident);
                 try {
                     callbackBinder.opCompleteForUser(getBackupUserId(), token, 0);
@@ -1296,6 +1306,16 @@
             }
         }
 
+        private static void clearUnconsumedDataFromPipe(ParcelFileDescriptor data, long size) {
+            try (FileInputStream in = new FileInputStream(data.getFileDescriptor())) {
+                if (in.available() > 0) {
+                    in.skip(size);
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to clear unconsumed data from pipe.", e);
+            }
+        }
+
         @Override
         public void doRestoreFinished(int token, IBackupManager callbackBinder) {
             final long ident = Binder.clearCallingIdentity();
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
new file mode 100644
index 0000000..ab00891
--- /dev/null
+++ b/core/java/android/app/multitasking.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+    name: "enable_pip_ui_state_callback_on_entering"
+    namespace: "multitasking"
+    description: "Enables PiP UI state callback on entering"
+    bug: "303718131"
+}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index d11c6c5..a5d4a14 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -43,3 +43,10 @@
   description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()"
   bug: "309920145"
 }
+
+flag {
+  name: "category_voicemail"
+  namespace: "wear_sysui"
+  description: "Adds a new voicemail category for notifications"
+  bug: "322806700"
+}
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index ec181da..1f19f81 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -907,7 +907,10 @@
 
     private InteractionHandler getHandler(InteractionHandler handler) {
         return (view, pendingIntent, response) -> {
-            AppWidgetManager.getInstance(mContext).noteAppWidgetTapped(mAppWidgetId);
+            AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+            if (manager != null) {
+                manager.noteAppWidgetTapped(mAppWidgetId);
+            }
             if (handler != null) {
                 return handler.onInteraction(view, pendingIntent, response);
             } else {
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index d0c8be6..97fa2ba 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -17,11 +17,14 @@
 package android.companion.virtual;
 
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
 import android.content.Context;
 import android.os.Parcel;
@@ -164,6 +167,44 @@
         }
     }
 
+    /**
+     * Returns whether this device may have custom audio input device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public boolean hasCustomAudioInputSupport() {
+        try {
+            return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM;
+            // TODO(b/291735254): also check for a custom audio injection mix for this device id.
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns whether this device may have custom cameras.
+     *
+     * <p>Returning {@code true} does not necessarily mean that this device has cameras, it only
+     * means that a {@link android.hardware.camera2.CameraManager} instance created from a
+     * {@link Context} associated with this device will return this device's cameras, if any.</p>
+     *
+     * @see Context#getDeviceId()
+     * @see Context#createDeviceContext(int)
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public boolean hasCustomCameraSupport() {
+        try {
+            return mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA) == DEVICE_POLICY_CUSTOM;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 90d251b..a16e94a 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -63,6 +63,7 @@
 import android.os.Binder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.view.Surface;
 import android.view.WindowManager;
@@ -74,9 +75,11 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
 
@@ -389,6 +392,28 @@
     }
 
     /**
+     * Returns all current persistent device IDs, including the ones for which no virtual device
+     * exists, as long as one may have existed or can be created.
+     *
+     * @hide
+     */
+    // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId()
+    @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API)
+    @SystemApi
+    @NonNull
+    public Set<String> getAllPersistentDeviceIds() {
+        if (mService == null) {
+            Log.w(TAG, "Failed to retrieve persistent ids; no virtual device manager service.");
+            return Collections.emptySet();
+        }
+        try {
+            return new ArraySet<>(mService.getAllPersistentDeviceIds());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
      * {@link Context#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
      * device which is not a virtual device. {@code deviceId} must correspond to a virtual device
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index bde562d..33b1134 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -409,6 +409,7 @@
                     "packageName = " + mAttributionSourceState.packageName + ", " +
                     "attributionTag = " + mAttributionSourceState.attributionTag + ", " +
                     "token = " + mAttributionSourceState.token + ", " +
+                    "deviceId = " + mAttributionSourceState.deviceId + ", " +
                     "next = " + (mAttributionSourceState.next != null
                                     && mAttributionSourceState.next.length > 0
                             ? mAttributionSourceState.next[0] : null) +
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index b1173a2..b8d7543 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3657,8 +3657,8 @@
      * On Android {@link android.os.Build.VERSION_CODES#S} and later,
      * if the application is in a state where the service
      * can not be started (such as not in the foreground in a state when services are allowed),
-     * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown
-     * This excemption extends {@link IllegalStateException}, so apps can
+     * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown.
+     * This exception extends {@link IllegalStateException}, so apps can
      * use {@code catch (IllegalStateException)} to catch both.
      *
      * @see #startForegroundService(Intent)
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index a97de63..62db65f 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -128,4 +128,6 @@
 
     /** Unregister a callback, so that it won't be called when LauncherApps dumps. */
     void unRegisterDumpCallback(IDumpCallback cb);
+
+    void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
 }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 6dc8d47..380de96 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -840,7 +840,7 @@
 
     ArchivedPackageParcel getArchivedPackage(in String packageName, int userId);
 
-    Bitmap getArchivedAppIcon(String packageName, in UserHandle user);
+    Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName);
 
     boolean isAppArchivable(String packageName, in UserHandle user);
 }
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 1d2b1af..50be983 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1801,6 +1801,31 @@
         }
     }
 
+    /**
+     * Enable or disable different archive compatibility options of the launcher.
+     *
+     * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware
+     * that a certain app is archived. True by default.
+     * Launchers might want to disable this operation if they want to provide custom user experience
+     * to differentiate archived apps.
+     * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when
+     * they click an archived app, which explains that the app will be downloaded and restored in
+     * the background. True by default.
+     * Launchers might want to disable this operation if they provide sufficient, alternative user
+     * guidance to highlight that an unarchival is starting and ongoing once an archived app is
+     * tapped. E.g., this could be achieved by showing the unarchival progress around the icon.
+     */
+    @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
+    public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+            boolean enableUnarchivalConfirmation) {
+        try {
+            mService.setArchiveCompatibilityOptions(enableIconOverlay,
+                    enableUnarchivalConfirmation);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     /** @return position in mCallbacks for callback or -1 if not present. */
     private int findCallbackLocked(Callback callback) {
         if (callback == null) {
diff --git a/core/java/android/content/pm/verify/domain/OWNERS b/core/java/android/content/pm/verify/domain/OWNERS
index c669112..b451fe4 100644
--- a/core/java/android/content/pm/verify/domain/OWNERS
+++ b/core/java/android/content/pm/verify/domain/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 36137
+include /PACKAGE_MANAGER_OWNERS
 
-chiuwinson@google.com
-patb@google.com
-toddke@google.com
\ No newline at end of file
+wloh@google.com
\ No newline at end of file
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 0396443..1c36a88 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -1575,7 +1575,7 @@
         }
 
         @Override
-        public void opChanged(int op, int uid, String packageName) {
+        public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
             if (op == AppOpsManager.OP_PLAY_AUDIO) {
                 final Camera camera = mWeakCamera.get();
                 if (camera != null) {
diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
index 7c54a9b..509bcb8 100644
--- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
@@ -26,6 +26,7 @@
     Surface surface;
     int imageFormat;
     int capacity;
+    long usage;
 
     const int TYPE_SURFACE = 0;
     const int TYPE_IMAGEREADER = 1;
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 98bc311..f6c8f36 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -1182,7 +1182,8 @@
                     return null;
                 }
                 ImageReader reader = ImageReader.newInstance(output.size.width,
-                        output.size.height, output.imageFormat, output.capacity);
+                        output.size.height, output.imageFormat, output.capacity,
+                        output.usage);
                 mReaderMap.put(output.outputId.id, reader);
                 return reader.getSurface();
             case CameraOutputConfig.TYPE_MULTIRES_IMAGEREADER:
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index fdbd319..89fa5fb 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -19,6 +19,7 @@
 import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
+import static com.android.hardware.input.Flags.touchpadTapDragging;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
 
 import android.Manifest;
@@ -303,6 +304,53 @@
     }
 
     /**
+     * Returns true if the feature flag for touchpad tap dragging is enabled.
+     *
+     * @hide
+     */
+    public static boolean isTouchpadTapDraggingFeatureFlagEnabled() {
+        return touchpadTapDragging();
+    }
+
+    /**
+     * Returns true if the touchpad should allow tap dragging.
+     *
+     * The returned value only applies to gesture-compatible touchpads.
+     *
+     * @param context The application context.
+     * @return Whether the touchpad should allow tap dragging.
+     *
+     * @hide
+     */
+    public static boolean useTouchpadTapDragging(@NonNull Context context) {
+        if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+            return false;
+        }
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.TOUCHPAD_TAP_DRAGGING, 0, UserHandle.USER_CURRENT) == 1;
+    }
+
+    /**
+     * Sets the tap dragging behavior for the touchpad.
+     *
+     * The new behavior is only applied to gesture-compatible touchpads.
+     *
+     * @param context The application context.
+     * @param enabled Will enable tap dragging if true, disable it if false
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setTouchpadTapDragging(@NonNull Context context, boolean enabled) {
+        if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+            return;
+        }
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.TOUCHPAD_TAP_DRAGGING, enabled ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
      * Returns true if the touchpad should use the right click zone.
      *
      * The returned value only applies to gesture-compatible touchpads.
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 0ed6569..e070fe5 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -33,7 +33,21 @@
 
 flag {
     namespace: "input_native"
+    name: "emoji_and_screenshot_keycodes_available"
+    description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
+    bug: "315307777"
+}
+
+flag {
+    namespace: "input_native"
     name: "keyboard_a11y_slow_keys_flag"
     description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user"
     bug: "294546335"
+}
+
+flag {
+    namespace: "input_native"
+    name: "touchpad_tap_dragging"
+    description: "Offers a setting to enable touchpad tap dragging"
+    bug: "321978150"
 }
\ No newline at end of file
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index db14c08..da6b9c2 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -507,10 +507,16 @@
      *
      * @param key The key the value is stored under.
      * @return a bitmap identifier or 0 if it's missing.
-     * @hide This API is not thoroughly elaborated yet
+     * @throws NullPointerException if metadata key is {@code null}
+     * @throws IllegalArgumentException if the metadata with the key is not found in
+     * metadata or the key is not of bitmap-key type
      */
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     public int getBitmapId(@NonNull String key) {
-        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0;
+        Objects.requireNonNull(key, "Metadata key can not be null");
+        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
+            throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
+        }
         return getInt(key);
     }
 
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 9b2bcde..7c5c003 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -17,6 +17,7 @@
 package android.hardware.radio;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -332,29 +333,30 @@
     public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
 
     /**
-     * Retrieves a {@link Bitmap} for the given image ID or null,
+     * Retrieves a {@link Bitmap} for the given image ID or throw {@link IllegalArgumentException},
      * if the image was missing from the tuner.
      *
      * <p>This involves doing a call to the tuner, so the bitmap should be cached
      * on the application side.
      *
-     * <p>If the method returns null for non-zero ID, it means the image was
-     * updated on the tuner side. There is a race conditon between fetching
-     * image for an old ID and tuner updating the image (and cleaning up the
+     * <p>If the method throws {@link IllegalArgumentException} for non-zero ID, it
+     * means the image was updated on the tuner side. There is a race condition between
+     * fetching image for an old ID and tuner updating the image (and cleaning up the
      * old image). In such case, a new ProgramInfo with updated image id will
      * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
      * callback.
      *
      * @param id The image identifier, retrieved with
      *           {@link RadioMetadata#getBitmapId(String)}.
-     * @return A {@link Bitmap} or null.
-     * @throws IllegalArgumentException if id==0
-     * @hide This API is not thoroughly elaborated yet
+     * @return A {@link Bitmap} for the given image ID.
+     * @throws IllegalArgumentException if id is 0 or the referenced image id no longer exists.
      */
-    @SuppressWarnings("HiddenAbstractMethod")
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
-    public abstract @Nullable Bitmap getMetadataImage(int id);
-
+    public @NonNull Bitmap getMetadataImage(int id) {
+        throw new UnsupportedOperationException(
+                "Getting metadata image must be implemented in child classes");
+    }
     /**
      * Initiates a background scan to update internally cached program list.
      *
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index ba31ca3..63b2d4c 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -16,6 +16,7 @@
 
 package android.hardware.radio;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Bitmap;
 import android.os.RemoteException;
@@ -251,10 +252,18 @@
     }
 
     @Override
-    @Nullable
+    @NonNull
     public Bitmap getMetadataImage(int id) {
+        if (id == 0) {
+            throw new IllegalArgumentException("Invalid metadata image id 0");
+        }
         try {
-            return mTuner.getImage(id);
+            Bitmap bitmap = mTuner.getImage(id);
+            if (bitmap == null) {
+                throw new IllegalArgumentException("Metadata image with id " + id
+                        + " is not available");
+            }
+            return bitmap;
         } catch (RemoteException e) {
             throw new RuntimeException("Service died", e);
         }
diff --git a/core/java/android/net/flags.aconfig b/core/java/android/net/flags.aconfig
index 0ad1804..311dc09 100644
--- a/core/java/android/net/flags.aconfig
+++ b/core/java/android/net/flags.aconfig
@@ -1,50 +1,11 @@
-package: "com.android.net.flags"
+package: "android.net.platform.flags"
 
-flag {
-  name: "track_multiple_network_activities"
-  namespace: "android_core_networking"
-  description: "NetworkActivityTracker tracks multiple networks including non default networks"
-  bug: "267870186"
-}
-
-flag {
-  name: "forbidden_capability"
-  namespace: "android_core_networking"
-  description: "This flag controls the forbidden capability API"
-  bug: "302997505"
-}
-
-flag {
-  name: "set_data_saver_via_cm"
-  namespace: "android_core_networking"
-  description: "Set data saver through ConnectivityManager API"
-  bug: "297836825"
-}
-
-flag {
-  name: "support_is_uid_networking_blocked"
-  namespace: "android_core_networking"
-  description: "This flag controls whether isUidNetworkingBlocked is supported"
-  bug: "297836825"
-}
-
-flag {
-  name: "basic_background_restrictions_enabled"
-  namespace: "android_core_networking"
-  description: "Block network access for apps in a low importance background state"
-  bug: "304347838"
-}
-
-flag {
-  name: "register_nsd_offload_engine"
-  namespace: "android_core_networking"
-  description: "The flag controls the access for registerOffloadEngine API in NsdManager"
-  bug: "294777050"
-}
+# This file contains aconfig flags used from platform code
+# Flags used for module APIs must be in aconfig files under each modules
 
 flag {
   name: "ipsec_transform_state"
-  namespace: "android_core_networking_ipsec"
+  namespace: "core_networking_ipsec"
   description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
   bug: "308011229"
 }
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 05b7827f..b7556df 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -292,7 +292,7 @@
         sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking);
     }
 
-    private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+    private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
 
     @android.ravenwood.annotation.RavenwoodKeepWholeClass
     private static class IdentitySupplier implements Supplier<SomeArgs> {
diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java
index fcd5731..36730cb 100644
--- a/core/java/android/os/HandlerThread.java
+++ b/core/java/android/os/HandlerThread.java
@@ -35,6 +35,7 @@
     public HandlerThread(String name) {
         super(name);
         mPriority = Process.THREAD_PRIORITY_DEFAULT;
+        onCreated();
     }
     
     /**
@@ -46,8 +47,21 @@
     public HandlerThread(String name, int priority) {
         super(name);
         mPriority = priority;
+        onCreated();
     }
-    
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodReplace
+    protected void onCreated() {
+    }
+
+    /** @hide */
+    protected void onCreated$ravenwood() {
+        // Mark ourselves as daemon to enable tests to terminate quickly when finished, despite
+        // any HandlerThread instances that may be lingering around
+        setDaemon(true);
+    }
+
     /**
      * Call back method that can be explicitly overridden if needed to execute some
      * setup before Looper loops.
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 1f3a162..7020a38 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -20,6 +20,7 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -844,7 +845,7 @@
         return "amd64".equals(System.getProperty("os.arch"));
     }
 
-    private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+    private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
 
     /** @hide */
     @android.ravenwood.annotation.RavenwoodKeep
@@ -1122,7 +1123,8 @@
      * priority.
      */
     @android.ravenwood.annotation.RavenwoodReplace
-    public static final native void setThreadPriority(int tid, int priority)
+    public static final native void setThreadPriority(int tid,
+            @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
             throws IllegalArgumentException, SecurityException;
 
     /** @hide */
@@ -1288,7 +1290,8 @@
      * @see #setThreadPriority(int, int)
      */
     @android.ravenwood.annotation.RavenwoodReplace
-    public static final native void setThreadPriority(int priority)
+    public static final native void setThreadPriority(
+            @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
             throws IllegalArgumentException, SecurityException;
 
     /** @hide */
@@ -1310,6 +1313,7 @@
      * <var>tid</var> does not exist.
      */
     @android.ravenwood.annotation.RavenwoodReplace
+    @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST)
     public static final native int getThreadPriority(int tid)
             throws IllegalArgumentException;
 
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 5e7549f..4b16c1d 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -28,6 +28,7 @@
  * The test code may use {@link #next()} to acquire messages that have been queued to this
  * {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class TestLooperManager {
 
     private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>();
diff --git a/core/java/android/os/incremental/OWNERS b/core/java/android/os/incremental/OWNERS
index 47eee64..a925564 100644
--- a/core/java/android/os/incremental/OWNERS
+++ b/core/java/android/os/incremental/OWNERS
@@ -1,6 +1,4 @@
 # Bug component: 554432
-alexbuy@google.com
-schfan@google.com
-toddke@google.com
+include /PACKAGE_MANAGER_OWNERS
+
 zyy@google.com
-patb@google.com
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 9587db1..3a57e84 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1669,12 +1669,6 @@
         }
     }
 
-    /** {@hide} */
-    @TestApi
-    public static boolean isUserKeyUnlocked(int userId) {
-        return isCeStorageUnlocked(userId);
-    }
-
     /**
      * Returns true if the user's credential-encrypted (CE) storage is unlocked.
      *
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index 92e4967..bcdb982 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -35,6 +35,7 @@
 import android.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 
 /**
  * List of device-specific internal vibration configuration loaded from platform config.xml.
@@ -51,6 +52,8 @@
     private final float mHapticChannelMaxVibrationAmplitude;
     private final int mRampStepDurationMs;
     private final int mRampDownDurationMs;
+    private final int mRequestVibrationParamsTimeoutMs;
+    private final int[] mRequestVibrationParamsForUsages;
 
     private final boolean mIgnoreVibrationsOnWirelessCharger;
 
@@ -75,6 +78,10 @@
                 com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0);
         mRampStepDurationMs = loadInteger(resources,
                 com.android.internal.R.integer.config_vibrationWaveformRampStepDuration, 0);
+        mRequestVibrationParamsTimeoutMs = loadInteger(resources,
+                com.android.internal.R.integer.config_requestVibrationParamsTimeout, 0);
+        mRequestVibrationParamsForUsages = loadIntArray(resources,
+                com.android.internal.R.array.config_requestVibrationParamsForUsages);
 
         mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources,
                 com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false);
@@ -115,6 +122,10 @@
         return res != null ? res.getBoolean(resId) : defaultValue;
     }
 
+    private static int[] loadIntArray(@Nullable Resources res, int resId) {
+        return res != null ? res.getIntArray(resId) : new int[0];
+    }
+
     /**
      * Return the maximum amplitude the vibrator can play using the audio haptic channels.
      *
@@ -140,6 +151,23 @@
     }
 
     /**
+     * The duration, in milliseconds, that the vibrator control service will wait for new
+     * vibration params.
+     */
+    public int getRequestVibrationParamsTimeoutMs() {
+        return Math.max(mRequestVibrationParamsTimeoutMs, 0);
+    }
+
+    /**
+     * The list of usages that should request vibration params before they are played. These
+     * usages don't have strong latency requirements, e.g. ringtone and notification, and can be
+     * slightly delayed.
+     */
+    public int[] getRequestVibrationParamsForUsages() {
+        return mRequestVibrationParamsForUsages;
+    }
+
+    /**
      * The duration, in milliseconds, that should be applied to convert vibration effect's
      * {@link android.os.vibrator.RampSegment} to a {@link android.os.vibrator.StepSegment} on
      * devices without PWLE support.
@@ -204,6 +232,9 @@
                 + ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity
                 + ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity
                 + ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
+                + ", mRequestVibrationParamsTimeoutMs=" + mRequestVibrationParamsTimeoutMs
+                + ", mRequestVibrationParamsForUsages=" + Arrays.toString(
+                getRequestVibrationParamsForUsagesNames())
                 + "}";
     }
 
@@ -220,4 +251,14 @@
         pw.println("rampDownDurationMs = " + mRampDownDurationMs);
         pw.decreaseIndent();
     }
+
+    private String[] getRequestVibrationParamsForUsagesNames() {
+        int usagesCount = mRequestVibrationParamsForUsages.length;
+        String[] names = new String[usagesCount];
+        for (int i = 0; i < usagesCount; i++) {
+            names[i] = VibrationAttributes.usageToString(mRequestVibrationParamsForUsages[i]);
+        }
+
+        return names;
+    }
 }
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 5d6dfc7..7d127ad 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -2102,7 +2102,7 @@
                 PhoneAccountHandle accountHandle) {
             TelecomManager tm = null;
             try {
-                tm = TelecomManager.from(context);
+                tm = context.getSystemService(TelecomManager.class);
             } catch (UnsupportedOperationException e) {
                 if (VERBOSE_LOG) {
                     Log.v(LOG_TAG, "No TelecomManager found to get account address.");
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e238080..524b733 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6049,6 +6049,13 @@
         public static final String TOUCHPAD_TAP_TO_CLICK = "touchpad_tap_to_click";
 
         /**
+         * Whether to enable tap dragging on touchpads.
+         *
+         * @hide
+         */
+        public static final String TOUCHPAD_TAP_DRAGGING = "touchpad_tap_dragging";
+
+        /**
          * Whether to enable a right-click zone on touchpads.
          *
          * When set to 1, pressing to click in a section on the right-hand side of the touchpad will
@@ -6270,6 +6277,7 @@
             PRIVATE_SETTINGS.add(TOUCHPAD_POINTER_SPEED);
             PRIVATE_SETTINGS.add(TOUCHPAD_NATURAL_SCROLLING);
             PRIVATE_SETTINGS.add(TOUCHPAD_TAP_TO_CLICK);
+            PRIVATE_SETTINGS.add(TOUCHPAD_TAP_DRAGGING);
             PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE);
             PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION);
             PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index d47ff2e..2841dc0 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -3202,7 +3202,7 @@
         /**
          * The infrastructure bitmask which the APN can be used on. For example, some APNs can only
          * be used when the device is on cellular, on satellite, or both. The default value is
-         * 1 (INFRASTRUCTURE_CELLULAR).
+         * 3 (INFRASTRUCTURE_CELLULAR | INFRASTRUCTURE_SATELLITE).
          *
          * <P>Type: INTEGER</P>
          * @hide
@@ -4927,7 +4927,7 @@
         /**
          * TelephonyProvider column name for satellite attach enabled for carrier. The value of this
          * column is set based on user settings.
-         * By default, it's disabled.
+         * By default, it's enabled.
          *
          * @hide
          */
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index 533d459..8bd6c85 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -1,7 +1,7 @@
 # Bug component: 36824
 
 brambonne@google.com
-brufino@google.com
+eranm@google.com
 jeffv@google.com
 
 per-file *NetworkSecurityPolicy.java = file:net/OWNERS
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index 3008b8d..446fe3d 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -8,14 +8,6 @@
 }
 
 flag {
-  name: "notification_lifetime_extension_refactor"
-    namespace: "systemui"
-    description: "Enables moving notification lifetime extension management from SystemUI to "
-        "Notification Manager Service"
-    bug: "299448097"
-}
-
-flag {
   name: "redact_sensitive_notifications_from_untrusted_listeners"
   namespace: "systemui"
   description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java
index 94c8eaf..783f3b7 100644
--- a/core/java/android/text/FontConfig.java
+++ b/core/java/android/text/FontConfig.java
@@ -26,7 +26,6 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.fonts.FontFamily.Builder.VariableFontFamilyType;
 import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontVariationAxis;
 import android.icu.util.ULocale;
@@ -240,6 +239,23 @@
         private final @IntRange(from = 0) int mIndex;
         private final @NonNull String mFontVariationSettings;
         private final @Nullable String mFontFamilyName;
+        private final @VarTypeAxes int mVarTypeAxes;
+
+        /** @hide */
+        @Retention(SOURCE)
+        @IntDef(prefix = { "VAR_TYPE_AXES_" }, value = {
+                VAR_TYPE_AXES_NONE,
+                VAR_TYPE_AXES_WGHT,
+                VAR_TYPE_AXES_ITAL,
+        })
+        public @interface VarTypeAxes {}
+
+        /** @hide */
+        public static final int VAR_TYPE_AXES_NONE = 0;
+        /** @hide */
+        public static final int VAR_TYPE_AXES_WGHT = 1;
+        /** @hide */
+        public static final int VAR_TYPE_AXES_ITAL = 2;
 
         /**
          * Construct a Font instance.
@@ -248,7 +264,8 @@
          */
         public Font(@NonNull File file, @Nullable File originalFile, @NonNull String postScriptName,
                 @NonNull FontStyle style, @IntRange(from = 0) int index,
-                @NonNull String fontVariationSettings, @Nullable String fontFamilyName) {
+                @NonNull String fontVariationSettings, @Nullable String fontFamilyName,
+                @VarTypeAxes int varTypeAxes) {
             mFile = file;
             mOriginalFile = originalFile;
             mPostScriptName = postScriptName;
@@ -256,6 +273,7 @@
             mIndex = index;
             mFontVariationSettings = fontVariationSettings;
             mFontFamilyName = fontFamilyName;
+            mVarTypeAxes = varTypeAxes;
         }
 
         @Override
@@ -273,6 +291,7 @@
             dest.writeInt(mIndex);
             dest.writeString8(mFontVariationSettings);
             dest.writeString8(mFontFamilyName);
+            dest.writeInt(mVarTypeAxes);
         }
 
         public static final @NonNull Creator<Font> CREATOR = new Creator<Font>() {
@@ -288,9 +307,10 @@
                 int index = source.readInt();
                 String varSettings = source.readString8();
                 String fallback = source.readString8();
+                int varTypeAxes = source.readInt();
 
                 return new Font(path, originalPath, postScriptName, new FontStyle(weight, slant),
-                        index, varSettings, fallback);
+                        index, varSettings, fallback, varTypeAxes);
             }
 
             @Override
@@ -366,6 +386,15 @@
         }
 
         /**
+         * Returns the list of supported axes tags for variable family type resolution.
+         *
+         * @hide
+         */
+        public @VarTypeAxes int getVarTypeAxes() {
+            return mVarTypeAxes;
+        }
+
+        /**
          * Returns the list of axes associated to this font.
          * @deprecated Use getFontVariationSettings
          * @hide
@@ -408,13 +437,14 @@
                     && Objects.equals(mOriginalFile, font.mOriginalFile)
                     && Objects.equals(mStyle, font.mStyle)
                     && Objects.equals(mFontVariationSettings, font.mFontVariationSettings)
-                    && Objects.equals(mFontFamilyName, font.mFontFamilyName);
+                    && Objects.equals(mFontFamilyName, font.mFontFamilyName)
+                    && mVarTypeAxes == font.mVarTypeAxes;
         }
 
         @Override
         public int hashCode() {
             return Objects.hash(mFile, mOriginalFile, mStyle, mIndex, mFontVariationSettings,
-                    mFontFamilyName);
+                    mFontFamilyName, mVarTypeAxes);
         }
 
         @Override
@@ -426,6 +456,7 @@
                     + ", mIndex=" + mIndex
                     + ", mFontVariationSettings='" + mFontVariationSettings + '\''
                     + ", mFontFamilyName='" + mFontFamilyName + '\''
+                    + ", mVarTypeAxes='" + mVarTypeAxes + '\''
                     + '}';
         }
     }
@@ -549,7 +580,6 @@
         private final @NonNull List<Font> mFonts;
         private final @NonNull LocaleList mLocaleList;
         private final @Variant int mVariant;
-        private final int mVariableFontFamilyType;
 
         /** @hide */
         @Retention(SOURCE)
@@ -589,11 +619,10 @@
          * @hide Only system server can create this instance and passed via IPC.
          */
         public FontFamily(@NonNull List<Font> fonts, @NonNull LocaleList localeList,
-                @Variant int variant, int variableFontFamilyType) {
+                @Variant int variant) {
             mFonts = fonts;
             mLocaleList = localeList;
             mVariant = variant;
-            mVariableFontFamilyType = variableFontFamilyType;
         }
 
         /**
@@ -644,20 +673,6 @@
             return mVariant;
         }
 
-        /**
-         * Returns the font family type.
-         *
-         * @see Builder#VARIABLE_FONT_FAMILY_TYPE_NONE
-         * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL
-         * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY
-         * @see Builder#VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT
-         * @hide
-         * @return variable font family type.
-         */
-        public @VariableFontFamilyType int getVariableFontFamilyType() {
-            return mVariableFontFamilyType;
-        }
-
         @Override
         public int describeContents() {
             return 0;
@@ -668,7 +683,6 @@
             dest.writeTypedList(mFonts, flags);
             dest.writeString8(mLocaleList.toLanguageTags());
             dest.writeInt(mVariant);
-            dest.writeInt(mVariableFontFamilyType);
         }
 
         public static final @NonNull Creator<FontFamily> CREATOR = new Creator<FontFamily>() {
@@ -679,10 +693,8 @@
                 source.readTypedList(fonts, Font.CREATOR);
                 String langTags = source.readString8();
                 int variant = source.readInt();
-                int varFamilyType = source.readInt();
 
-                return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant,
-                        varFamilyType);
+                return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant);
             }
 
             @Override
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 6e45fea..f3e0ea7 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -96,3 +96,10 @@
   description: "A feature flag for fixing the crash while delete text in insert mode."
   bug: "314254153"
 }
+
+flag {
+  name: "insert_mode_not_update_selection"
+  namespace: "text"
+  description: "Fix that InputService#onUpdateSelection is not called when insert mode gesture is performed."
+  bug: "300850862"
+}
diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java
index c0ceb9ea..9ecb4cb 100644
--- a/core/java/android/util/Slog.java
+++ b/core/java/android/util/Slog.java
@@ -217,7 +217,6 @@
      * @see Log#wtf(String, String)
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    @android.ravenwood.annotation.RavenwoodThrow
     public static int wtf(@Nullable String tag, @NonNull String msg) {
         return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, false, true);
     }
@@ -263,7 +262,6 @@
      *
      * @see Log#wtf(String, Throwable)
      */
-    @android.ravenwood.annotation.RavenwoodThrow
     public static int wtf(@Nullable String tag, @Nullable Throwable tr) {
         return Log.wtf(Log.LOG_ID_SYSTEM, tag, tr.getMessage(), tr, false, true);
     }
@@ -284,7 +282,6 @@
      * @see Log#wtf(String, String, Throwable)
      */
     @UnsupportedAppUsage
-    @android.ravenwood.annotation.RavenwoodThrow
     public static int wtf(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
         return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, tr, false, true);
     }
diff --git a/core/java/android/view/HdrRenderState.java b/core/java/android/view/HdrRenderState.java
index 2fbbf48..eadc507 100644
--- a/core/java/android/view/HdrRenderState.java
+++ b/core/java/android/view/HdrRenderState.java
@@ -31,9 +31,11 @@
 
     private final ViewRootImpl mViewRoot;
 
+    private boolean mIsHdrEnabled = false;
     private boolean mIsListenerRegistered = false;
     private boolean mUpdateHdrSdrRatioInfo = false;
     private float mDesiredHdrSdrRatio = 1f;
+    private float mTargetDesiredHdrSdrRatio = 1f;
     private float mTargetHdrSdrRatio = 1f;
     private float mRenderHdrSdrRatio = 1f;
     private float mPreviousRenderRatio = 1f;
@@ -50,7 +52,7 @@
     }
 
     boolean isHdrEnabled() {
-        return mDesiredHdrSdrRatio >= 1.01f;
+        return mIsHdrEnabled;
     }
 
     void stopListening() {
@@ -75,9 +77,7 @@
         final float maxStep = timeDelta * TRANSITION_PER_MS;
         mLastUpdateMillis = frameTimeMillis;
         if (hasUpdate && FLAG_ANIMATE_ENABLED) {
-            if (mTargetHdrSdrRatio == 1.0f) {
-                mPreviousRenderRatio = mTargetHdrSdrRatio;
-            } else {
+            if (isHdrEnabled()) {
                 float delta = mTargetHdrSdrRatio - mPreviousRenderRatio;
                 if (delta > maxStep) {
                     mRenderHdrSdrRatio = mPreviousRenderRatio + maxStep;
@@ -85,6 +85,19 @@
                     mViewRoot.invalidate();
                 }
                 mPreviousRenderRatio = mRenderHdrSdrRatio;
+
+                if (mTargetDesiredHdrSdrRatio < mDesiredHdrSdrRatio) {
+                    mDesiredHdrSdrRatio = Math.max(mTargetDesiredHdrSdrRatio,
+                            mDesiredHdrSdrRatio - maxStep);
+                    if (mDesiredHdrSdrRatio != mTargetDesiredHdrSdrRatio) {
+                        mUpdateHdrSdrRatioInfo = true;
+                        mViewRoot.invalidate();
+                    }
+                }
+
+            } else {
+                mPreviousRenderRatio = mTargetHdrSdrRatio;
+                mDesiredHdrSdrRatio = mTargetDesiredHdrSdrRatio;
             }
         }
         return hasUpdate;
@@ -99,15 +112,23 @@
     }
 
     void forceUpdateHdrSdrRatio() {
-        mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mViewRoot.mDisplay.getHdrSdrRatio());
+        if (isHdrEnabled()) {
+            mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio,
+                    mViewRoot.mDisplay.getHdrSdrRatio());
+        } else {
+            mTargetHdrSdrRatio = 1.0f;
+        }
         mUpdateHdrSdrRatioInfo = true;
     }
 
-    void setDesiredHdrSdrRatio(float desiredRatio) {
+    void setDesiredHdrSdrRatio(boolean isHdrEnabled, float desiredRatio) {
+        mIsHdrEnabled = isHdrEnabled;
         mLastUpdateMillis = SystemClock.uptimeMillis();
-        // TODO: When decreasing the desired ratio we need to animate it downwards
-        if (desiredRatio != mDesiredHdrSdrRatio) {
-            mDesiredHdrSdrRatio = desiredRatio;
+        if (desiredRatio != mTargetDesiredHdrSdrRatio) {
+            mTargetDesiredHdrSdrRatio = desiredRatio;
+            if (mTargetDesiredHdrSdrRatio > mDesiredHdrSdrRatio || !FLAG_ANIMATE_ENABLED) {
+                mDesiredHdrSdrRatio = mTargetDesiredHdrSdrRatio;
+            }
             forceUpdateHdrSdrRatio();
             mViewRoot.invalidate();
 
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index c6601e8d..1ee9509 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -19,6 +19,7 @@
 import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
 import static android.view.Display.INVALID_DISPLAY;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -31,6 +32,8 @@
 import android.util.SparseIntArray;
 import android.view.KeyCharacterMap.KeyData;
 
+import com.android.hardware.input.Flags;
+
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -384,7 +387,13 @@
     public static final int KEYCODE_META_RIGHT      = 118;
     /** Key code constant: Function modifier key. */
     public static final int KEYCODE_FUNCTION        = 119;
-    /** Key code constant: System Request / Print Screen key. */
+    /**
+     * Key code constant: System Request / Print Screen key.
+     *
+     * This key is sent to the app first and only if the app doesn't handle it, the framework
+     * handles it (to take a screenshot), unlike {@code KEYCODE_TAKE_SCREENSHOT} which is
+     * fully handled by the framework.
+     */
     public static final int KEYCODE_SYSRQ           = 120;
     /** Key code constant: Break / Pause key. */
     public static final int KEYCODE_BREAK           = 121;
@@ -921,14 +930,25 @@
      * User customizable key #4.
      */
     public static final int KEYCODE_MACRO_4 = 316;
-
+    /** Key code constant: To open emoji picker */
+    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+    public static final int KEYCODE_EMOJI_PICKER = 317;
+    /**
+     * Key code constant: To take a screenshot
+     *
+     * This key is fully handled by the framework and will not be sent to the foreground app,
+     * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app
+     * doesn't handle it, the framework handles it (to take a screenshot).
+     */
+    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+    public static final int KEYCODE_SCREENSHOT = 318;
 
    /**
      * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
      * @hide
      */
     @TestApi
-    public static final int LAST_KEYCODE = KEYCODE_MACRO_4;
+    public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT;
 
     // NOTE: If you add a new keycode here you must also add it to:
     //  isSystem()
diff --git a/core/java/android/view/MotionPredictor.java b/core/java/android/view/MotionPredictor.java
index 27af300..db2efaa 100644
--- a/core/java/android/view/MotionPredictor.java
+++ b/core/java/android/view/MotionPredictor.java
@@ -20,6 +20,8 @@
 import android.annotation.Nullable;
 import android.content.Context;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import libcore.util.NativeAllocationRegistry;
 
 /**
@@ -57,11 +59,21 @@
      * @param context The context for the predictions
      */
     public MotionPredictor(@NonNull Context context) {
-        mIsPredictionEnabled = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_enableMotionPrediction);
-        final int offsetNanos = context.getResources().getInteger(
-                com.android.internal.R.integer.config_motionPredictionOffsetNanos);
-        mPtr = nativeInitialize(offsetNanos);
+        this(
+                context.getResources().getBoolean(
+                        com.android.internal.R.bool.config_enableMotionPrediction),
+                context.getResources().getInteger(
+                        com.android.internal.R.integer.config_motionPredictionOffsetNanos));
+    }
+
+    /**
+     * Internal constructor for testing.
+     * @hide
+     */
+    @VisibleForTesting
+    public MotionPredictor(boolean isPredictionEnabled, int motionPredictionOffsetNanos) {
+        mIsPredictionEnabled = isPredictionEnabled;
+        mPtr = nativeInitialize(motionPredictionOffsetNanos);
         RegistryHolder.REGISTRY.registerNativeAllocation(this, mPtr);
     }
 
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 4840f00..5249fd5 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -538,8 +538,8 @@
     }
 
     private void addWindowToken(WindowManager.LayoutParams attrs) {
-        final WindowManagerImpl wm =
-                (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
+        final WindowManager wm =
+                (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
         attrs.token = wm.getDefaultToken();
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7bc832e..c18aeee 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -399,6 +399,8 @@
     @Nullable
     private ContentObserver mForceInvertObserver;
 
+    private static final int INVALID_VALUE = Integer.MIN_VALUE;
+    private int mForceInvertEnabled = INVALID_VALUE;
     /**
      * Callback for notifying about global configuration changes.
      */
@@ -1604,6 +1606,23 @@
         }
     }
 
+    private boolean isForceInvertEnabled() {
+        if (mForceInvertEnabled == INVALID_VALUE) {
+            reloadForceInvertEnabled();
+        }
+        return mForceInvertEnabled == 1;
+    }
+
+    private void reloadForceInvertEnabled() {
+        if (forceInvertColor()) {
+            mForceInvertEnabled = Settings.Secure.getIntForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
+                    /* def= */ 0,
+                    UserHandle.myUserId());
+        }
+    }
+
     /**
      * Register any kind of listeners if setView was success.
      */
@@ -1630,6 +1649,7 @@
                 mForceInvertObserver = new ContentObserver(mHandler) {
                     @Override
                     public void onChange(boolean selfChange) {
+                        reloadForceInvertEnabled();
                         updateForceDarkMode();
                     }
                 };
@@ -1850,16 +1870,11 @@
     @VisibleForTesting
     public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
         if (forceInvertColor()) {
-            boolean isForceInvertEnabled = Settings.Secure.getIntForUser(
-                    mContext.getContentResolver(),
-                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
-                    /* def= */ 0,
-                    UserHandle.myUserId()) == 1;
             // Force invert ignores all developer opt-outs.
             // We also ignore dark theme, since the app developer can override the user's preference
             // for dark mode in configuration.uiMode. Instead, we assume that the force invert
             // setting will be enabled at the same time dark theme is in the Settings app.
-            if (isForceInvertEnabled) {
+            if (isForceInvertEnabled()) {
                 return ForceDarkType.FORCE_INVERT_COLOR_DARK;
             }
         }
@@ -5809,9 +5824,11 @@
         if (mAttachInfo.mThreadedRenderer == null) {
             return;
         }
-        if ((colorMode == ActivityInfo.COLOR_MODE_HDR || colorMode == ActivityInfo.COLOR_MODE_HDR10)
-                && !mDisplay.isHdrSdrRatioAvailable()) {
+        boolean isHdr = colorMode == ActivityInfo.COLOR_MODE_HDR
+                || colorMode == ActivityInfo.COLOR_MODE_HDR10;
+        if (isHdr && !mDisplay.isHdrSdrRatioAvailable()) {
             colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT;
+            isHdr = false;
         }
         // TODO: Centralize this sanitization? Why do we let setting bad modes?
         // Alternatively, can we just let HWUI figure it out? Do we need to care here?
@@ -5824,7 +5841,7 @@
             desiredRatio = automaticRatio;
         }
 
-        mHdrRenderState.setDesiredHdrSdrRatio(desiredRatio);
+        mHdrRenderState.setDesiredHdrSdrRatio(isHdr, desiredRatio);
     }
 
     @Override
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 427d053..c788261 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -6109,4 +6109,12 @@
         throw new UnsupportedOperationException(
                 "getSurfaceControlInputClientToken is not implemented");
     }
+
+    /**
+     * @hide
+     */
+    default @NonNull IBinder getDefaultToken() {
+        throw new UnsupportedOperationException(
+                "getDefaultToken is not implemented");
+    }
 }
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 41d181c..5072ad7 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -458,7 +458,9 @@
         return null;
     }
 
-    IBinder getDefaultToken() {
+    @Override
+    @NonNull
+    public IBinder getDefaultToken() {
         return mDefaultToken;
     }
 
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 7782fd7..ef1bf5a 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -62,6 +62,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.IWindow;
+import android.view.SurfaceControl;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent.EventType;
 
@@ -2404,4 +2405,29 @@
             throw re.rethrowFromSystemServer();
         }
     }
+
+
+    /**
+     * Attaches a {@link android.view.SurfaceControl} containing an accessibility overlay to the
+     * specified display.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    public void attachAccessibilityOverlayToDisplay(
+            int displayId, @NonNull SurfaceControl surfaceControl) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.attachAccessibilityOverlayToDisplay_enforcePermission(
+                    displayId, surfaceControl);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 9c04c27..1c5d29e 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -31,6 +31,7 @@
 import android.view.InputEvent;
 import android.view.IWindow;
 import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
 
 /**
  * Interface implemented by the AccessibilityManagerService called by
@@ -136,4 +137,7 @@
         MagnificationSpec magnificationSpec;
     }
     WindowTransformationSpec getWindowTransformationSpec(int windowId);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
+    void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl);
 }
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index 2a3008a..5d3153c 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -34,3 +34,10 @@
     description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener."
     bug: "289080543"
 }
+
+flag {
+    name: "manage_device_policy_enabled"
+    namespace: "content_protection"
+    description: "If true, the APIs to manage content protection device policy will be enabled."
+    bug: "319477846"
+}
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
new file mode 100644
index 0000000..6938b29
--- /dev/null
+++ b/core/java/android/webkit/flags.aconfig
@@ -0,0 +1,9 @@
+package: "android.webkit"
+
+flag {
+    name: "update_service_ipc_wrapper"
+    namespace: "webview"
+    description: "New API: proper wrapper for IWebViewUpdateService"
+    bug: "319292658"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0654add..2433bd8 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -7597,14 +7597,6 @@
         }
 
         /**
-         * Append additional instructions to this {@link DrawInstructions} object.
-         */
-        @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
-        public void appendInstructions(@NonNull final byte[] instructions) {
-            mInstructions.add(instructions);
-        }
-
-        /**
          * Builder class for {@link DrawInstructions} objects.
          */
         @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index e812f85..90d5140 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -243,6 +243,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastMath;
 import com.android.internal.util.Preconditions;
+import com.android.text.flags.Flags;
 
 import libcore.util.EmptyArray;
 
@@ -539,7 +540,6 @@
 
     // System wide time for last cut, copy or text changed action.
     static long sLastCutCopyOrTextChangedTime;
-
     private ColorStateList mTextColor;
     private ColorStateList mHintTextColor;
     private ColorStateList mLinkTextColor;
@@ -2857,7 +2857,38 @@
         }
 
         if (updateText) {
-            setText(mText);
+            if (Flags.insertModeNotUpdateSelection()) {
+                // Update the transformation text.
+                if (mTransformation == null) {
+                    mTransformed = mText;
+                } else {
+                    mTransformed = mTransformation.getTransformation(mText, this);
+                }
+                if (mTransformed == null) {
+                    // Should not happen if the transformation method follows the non-null
+                    // postcondition.
+                    mTransformed = "";
+                }
+                final boolean isOffsetMapping = mTransformed instanceof OffsetMapping;
+
+                // If the mText is a Spannable and the new TransformationMethod needs to listen to
+                // its updates, apply the watcher on it.
+                if (mTransformation != null && mText instanceof Spannable
+                        && (!mAllowTransformationLengthChange || isOffsetMapping)) {
+                    Spannable sp = (Spannable) mText;
+                    final int priority = isOffsetMapping ? OFFSET_MAPPING_SPAN_PRIORITY : 0;
+                    sp.setSpan(mTransformation, 0, mText.length(),
+                            Spanned.SPAN_INCLUSIVE_INCLUSIVE
+                                    | (priority << Spanned.SPAN_PRIORITY_SHIFT));
+                }
+                if (mLayout != null) {
+                    nullLayouts();
+                    requestLayout();
+                    invalidate();
+                }
+            } else {
+                setText(mText);
+            }
         }
 
         if (hasPasswordTransformationMethod()) {
diff --git a/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl b/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
index ae6ad32..94bbd72 100644
--- a/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
@@ -19,5 +19,5 @@
 // Iterface to observe op active changes
 oneway interface IAppOpsActiveCallback {
     void opActiveChanged(int op, int uid, String packageName, String attributionTag,
-            boolean active, int attributionFlags, int attributionChainId);
+            int virtualDeviceId, boolean active, int attributionFlags, int attributionChainId);
 }
diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/core/java/com/android/internal/app/IAppOpsCallback.aidl
index 024ff66..3a9525c 100644
--- a/core/java/com/android/internal/app/IAppOpsCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsCallback.aidl
@@ -19,5 +19,5 @@
 // This interface is also used by native code, so must
 // be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h
 oneway interface IAppOpsCallback {
-    void opChanged(int op, int uid, String packageName);
+    void opChanged(int op, int uid, String packageName, String persistentDeviceId);
 }
diff --git a/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
index f3759e0..123f4f4 100644
--- a/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
@@ -18,5 +18,6 @@
 
 // Iterface to observe op note/checks of ops
 oneway interface IAppOpsNotedCallback {
-    void opNoted(int op, int uid, String packageName, String attributionTag, int flags, int mode);
+    void opNoted(int op, int uid, String packageName, String attributionTag, int virtualDeviceId,
+     int flags, int mode);
 }
diff --git a/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
index 06640cb..fdae37a 100644
--- a/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
@@ -18,6 +18,6 @@
 
 // Iterface to observe op starts
 oneway interface IAppOpsStartedCallback {
-    void opStarted(int op, int uid, String packageName, String attributionTag, int flags, int mode,
-    int startedType, int attributionFlags, int attributionChainId);
+    void opStarted(int op, int uid, String packageName, String attributionTag, int virtualDeviceId,
+     int flags, int mode, int startedType, int attributionFlags, int attributionChainId);
 }
diff --git a/core/java/com/android/internal/os/AndroidPrintStream.java b/core/java/com/android/internal/os/AndroidPrintStream.java
index a6e41ff..bb388bb 100644
--- a/core/java/com/android/internal/os/AndroidPrintStream.java
+++ b/core/java/com/android/internal/os/AndroidPrintStream.java
@@ -24,6 +24,7 @@
  *
  * {@hide}
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 class AndroidPrintStream extends LoggingPrintStream {
 
     private final int priority;
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index 28b98d6..cdac097 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -39,6 +39,7 @@
 
 import libcore.content.type.MimeMap;
 
+import java.io.PrintStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -50,6 +51,7 @@
  * public consumption.
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
 public class RuntimeInit {
     final static String TAG = "AndroidRuntime";
     final static boolean DEBUG = false;
@@ -67,7 +69,15 @@
 
     private static volatile ApplicationWtfHandler sDefaultApplicationWtfHandler;
 
+    /**
+     * Stored values of System.out and System.err before they've been replaced by
+     * redirectLogStreams(). Kept open here for other Ravenwood internals to use.
+     */
+    public static PrintStream sOut$ravenwood;
+    public static PrintStream sErr$ravenwood;
+
     private static final native void nativeFinishInit();
+
     private static final native void nativeSetExitWithoutCleanup(boolean exitWithoutCleanup);
 
     private static int Clog_e(String tag, String msg, Throwable tr) {
@@ -385,6 +395,7 @@
     /**
      * Redirect System.out and System.err to the Android log.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static void redirectLogStreams() {
         System.out.close();
         System.setOut(new AndroidPrintStream(Log.INFO, "System.out"));
@@ -392,6 +403,17 @@
         System.setErr(new AndroidPrintStream(Log.WARN, "System.err"));
     }
 
+    public static void redirectLogStreams$ravenwood() {
+        if (sOut$ravenwood == null) {
+            sOut$ravenwood = System.out;
+            System.setOut(new AndroidPrintStream(Log.INFO, "System.out"));
+        }
+        if (sErr$ravenwood == null) {
+            sErr$ravenwood = System.err;
+            System.setErr(new AndroidPrintStream(Log.WARN, "System.err"));
+        }
+    }
+
     /**
      * Report a serious error in the current process.  May or may not cause
      * the process to terminate (depends on system settings).
@@ -399,6 +421,7 @@
      * @param tag to record with the error
      * @param t exception describing the error site and conditions
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static void wtf(String tag, Throwable t, boolean system) {
         try {
             boolean exit = false;
@@ -436,6 +459,11 @@
         }
     }
 
+    public static void wtf$ravenwood(String tag, Throwable t, boolean system) {
+        // We've already emitted to logs, so there's nothing more to do here,
+        // as we don't have a DropBox pipeline configured
+    }
+
     /**
      * Set the default {@link ApplicationWtfHandler}, in case the ActivityManager is not ready yet.
      */
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 4e3b64c..b4f9ee3 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2583,7 +2583,7 @@
             mNavigationBarDividerColor = a.getColor(R.styleable.Window_navigationBarDividerColor,
                     Color.TRANSPARENT);
         }
-        if (!targetPreQ && !mEdgeToEdgeEnforced) {
+        if (!targetPreQ) {
             mEnsureStatusBarContrastWhenTransparent = a.getBoolean(
                     R.styleable.Window_enforceStatusBarContrast, false);
             mEnsureNavigationBarContrastWhenTransparent = a.getBoolean(
@@ -3966,9 +3966,6 @@
 
     @Override
     public void setStatusBarContrastEnforced(boolean ensureContrast) {
-        if (mEdgeToEdgeEnforced) {
-            return;
-        }
         mEnsureStatusBarContrastWhenTransparent = ensureContrast;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
@@ -3982,9 +3979,6 @@
 
     @Override
     public void setNavigationBarContrastEnforced(boolean enforceContrast) {
-        if (mEdgeToEdgeEnforced) {
-            return;
-        }
         mEnsureNavigationBarContrastWhenTransparent = enforceContrast;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 5351c6d..ed43b81 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -151,8 +151,17 @@
      * @param hidesStatusBar whether it is being hidden
      */
     void setTopAppHidesStatusBar(boolean hidesStatusBar);
-
+    /**
+     * Add a tile to the Quick Settings Panel to the first item in the QS Panel
+     * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+     */
     void addQsTile(in ComponentName tile);
+    /**
+     * Add a tile to the Quick Settings Panel
+     * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+     * @param end if true, the tile will be added at the end. If false, at the beginning.
+     */
+    void addQsTileToFrontOrEnd(in ComponentName tile, boolean end);
     void remQsTile(in ComponentName tile);
     void setQsTiles(in String[] tiles);
     void clickQsTile(in ComponentName tile);
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 656cc3e..3fc1683 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -284,7 +284,6 @@
                 "libscrypt_static",
                 "libstatssocket_lazy",
                 "libskia",
-                "libperfetto_client_experimental",
             ],
 
             shared_libs: [
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 3413ede..969e47b 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -25,6 +25,7 @@
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
 #include <jni.h>
+#include <media/AidlConversion.h>
 #include <media/AudioContainers.h>
 #include <media/AudioPolicy.h>
 #include <media/AudioSystem.h>
@@ -66,6 +67,11 @@
     jmethodID toArray;
 } gArrayListMethods;
 
+static jclass gIntArrayClass;
+static struct {
+    jmethodID add;
+} gIntArrayMethods;
+
 static jclass gBooleanClass;
 static jmethodID gBooleanCstor;
 
@@ -1607,6 +1613,48 @@
     return jStatus;
 }
 
+// From AudioDeviceInfo
+static const int GET_DEVICES_INPUTS = 0x0001;
+static const int GET_DEVICES_OUTPUTS = 0x0002;
+
+static int android_media_AudioSystem_getSupportedDeviceTypes(JNIEnv *env, jobject clazz,
+                                                             jint direction, jobject jDeviceTypes) {
+    if (jDeviceTypes == NULL) {
+        ALOGE("%s NULL Device Types IntArray", __func__);
+        return AUDIO_JAVA_BAD_VALUE;
+    }
+    if (!env->IsInstanceOf(jDeviceTypes, gIntArrayClass)) {
+        ALOGE("%s not an IntArray", __func__);
+        return AUDIO_JAVA_BAD_VALUE;
+    }
+
+    // Convert AudioManager.GET_DEVICES_ flags to AUDIO_PORT_ROLE_ constants
+    audio_port_role_t role;
+    if (direction == GET_DEVICES_INPUTS) {
+        role = AUDIO_PORT_ROLE_SOURCE;
+    } else if (direction == GET_DEVICES_OUTPUTS) {
+        role = AUDIO_PORT_ROLE_SINK;
+    } else {
+        ALOGE("%s invalid direction : 0x%X", __func__, direction);
+        return AUDIO_JAVA_BAD_VALUE;
+    }
+
+    std::vector<media::AudioPortFw> deviceList;
+    AudioSystem::listDeclaredDevicePorts(static_cast<media::AudioPortRole>(role), &deviceList);
+
+    // Walk the device list
+    for (const auto &device : deviceList) {
+        ConversionResult<audio_port_v7> result = aidl2legacy_AudioPortFw_audio_port_v7(device);
+
+        struct audio_port_v7 port = VALUE_OR_RETURN_STATUS(result);
+        assert(port.type == AUDIO_PORT_TYPE_DEVICE);
+
+        env->CallVoidMethod(jDeviceTypes, gIntArrayMethods.add, port.ext.device.type);
+    }
+
+    return AUDIO_JAVA_SUCCESS;
+}
+
 static int
 android_media_AudioSystem_createAudioPatch(JNIEnv *env, jobject clazz,
                                  jobjectArray jPatches, jobjectArray jSources, jobjectArray jSinks)
@@ -3184,6 +3232,8 @@
                                 android_media_AudioSystem_setAudioFlingerBinder),
          MAKE_JNI_NATIVE_METHOD("listAudioPorts", "(Ljava/util/ArrayList;[I)I",
                                 android_media_AudioSystem_listAudioPorts),
+         MAKE_JNI_NATIVE_METHOD("getSupportedDeviceTypes", "(ILandroid/util/IntArray;)I",
+                                android_media_AudioSystem_getSupportedDeviceTypes),
          MAKE_JNI_NATIVE_METHOD("createAudioPatch",
                                 "([Landroid/media/AudioPatch;[Landroid/media/"
                                 "AudioPortConfig;[Landroid/media/AudioPortConfig;)I",
@@ -3325,6 +3375,10 @@
     gArrayListMethods.add = GetMethodIDOrDie(env, arrayListClass, "add", "(Ljava/lang/Object;)Z");
     gArrayListMethods.toArray = GetMethodIDOrDie(env, arrayListClass, "toArray", "()[Ljava/lang/Object;");
 
+    jclass intArrayClass = FindClassOrDie(env, "android/util/IntArray");
+    gIntArrayClass = MakeGlobalRefOrDie(env, intArrayClass);
+    gIntArrayMethods.add = GetMethodIDOrDie(env, gIntArrayClass, "add", "(I)V");
+
     jclass booleanClass = FindClassOrDie(env, "java/lang/Boolean");
     gBooleanClass = MakeGlobalRefOrDie(env, booleanClass);
     gBooleanCstor = GetMethodIDOrDie(env, booleanClass, "<init>", "(Z)V");
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
index d710698..16c3ca9 100644
--- a/core/jni/android_tracing_PerfettoDataSource.cpp
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -25,7 +25,6 @@
 #include <perfetto/public/producer.h>
 #include <perfetto/public/protos/trace/test_event.pzc.h>
 #include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
 #include <utils/Log.h>
 #include <utils/RefBase.h>
 
diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h
index 4ddf1d8..61a7654 100644
--- a/core/jni/android_tracing_PerfettoDataSource.h
+++ b/core/jni/android_tracing_PerfettoDataSource.h
@@ -23,10 +23,10 @@
 #include <perfetto/public/producer.h>
 #include <perfetto/public/protos/trace/test_event.pzc.h>
 #include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
 #include <utils/Log.h>
 #include <utils/RefBase.h>
 
+#include <map>
 #include <sstream>
 #include <thread>
 
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
index e659bf1..3fbd5b3 100644
--- a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
@@ -25,7 +25,6 @@
 #include <perfetto/public/producer.h>
 #include <perfetto/public/protos/trace/test_event.pzc.h>
 #include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
 #include <utils/Log.h>
 #include <utils/RefBase.h>
 
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h
index d577655..ebb5259 100644
--- a/core/jni/android_tracing_PerfettoDataSourceInstance.h
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h
@@ -23,7 +23,6 @@
 #include <perfetto/public/producer.h>
 #include <perfetto/public/protos/trace/test_event.pzc.h>
 #include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
 #include <utils/Log.h>
 #include <utils/RefBase.h>
 
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
index ce72f58..f8c63c8 100644
--- a/core/jni/android_tracing_PerfettoProducer.cpp
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -23,7 +23,6 @@
 #include <perfetto/public/producer.h>
 #include <perfetto/public/protos/trace/test_event.pzc.h>
 #include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
 #include <utils/Log.h>
 #include <utils/RefBase.h>
 
diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp
index d4a7462..d11166f 100644
--- a/core/jni/android_view_InputChannel.cpp
+++ b/core/jni/android_view_InputChannel.cpp
@@ -203,8 +203,10 @@
     if (parcel) {
         bool isInitialized = parcel->readInt32();
         if (isInitialized) {
-            std::unique_ptr<InputChannel> inputChannel = std::make_unique<InputChannel>();
-            inputChannel->readFromParcel(parcel);
+            android::os::InputChannelCore parcelableChannel;
+            parcelableChannel.readFromParcel(parcel);
+            std::unique_ptr<InputChannel> inputChannel =
+                    InputChannel::create(std::move(parcelableChannel));
             NativeInputChannel* nativeInputChannel =
                     new NativeInputChannel(std::move(inputChannel));
             return reinterpret_cast<jlong>(nativeInputChannel);
@@ -228,7 +230,9 @@
         return;
     }
     parcel->writeInt32(1); // initialized
-    nativeInputChannel->getInputChannel()->writeToParcel(parcel);
+    android::os::InputChannelCore parcelableChannel;
+    nativeInputChannel->getInputChannel()->copyTo(parcelableChannel);
+    parcelableChannel.writeToParcel(parcel);
 }
 
 static jstring android_view_InputChannel_nativeGetName(JNIEnv* env, jobject obj, jlong channel) {
diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp
index f7d8152..f93b306 100644
--- a/core/jni/android_view_InputEventReceiver.cpp
+++ b/core/jni/android_view_InputEventReceiver.cpp
@@ -189,11 +189,11 @@
 void NativeInputEventReceiver::setFdEvents(int events) {
     if (mFdEvents != events) {
         mFdEvents = events;
-        auto&& fd = mInputConsumer.getChannel()->getFd();
+        const int fd = mInputConsumer.getChannel()->getFd();
         if (events) {
-            mMessageQueue->getLooper()->addFd(fd.get(), 0, events, this, nullptr);
+            mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
         } else {
-            mMessageQueue->getLooper()->removeFd(fd.get());
+            mMessageQueue->getLooper()->removeFd(fd);
         }
     }
 }
diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp
index 6bdf821..323f7b6 100644
--- a/core/jni/android_view_InputEventSender.cpp
+++ b/core/jni/android_view_InputEventSender.cpp
@@ -102,8 +102,8 @@
 }
 
 status_t NativeInputEventSender::initialize() {
-    auto&& receiveFd = mInputPublisher.getChannel()->getFd();
-    mMessageQueue->getLooper()->addFd(receiveFd.get(), 0, ALOOPER_EVENT_INPUT, this, NULL);
+    const int receiveFd = mInputPublisher.getChannel()->getFd();
+    mMessageQueue->getLooper()->addFd(receiveFd, 0, ALOOPER_EVENT_INPUT, this, NULL);
     return OK;
 }
 
@@ -112,7 +112,7 @@
         LOG(DEBUG) << "channel '" << getInputChannelName() << "' ~ Disposing input event sender.";
     }
 
-    mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel()->getFd().get());
+    mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel()->getFd());
 }
 
 status_t NativeInputEventSender::sendKeyEvent(uint32_t seq, const KeyEvent* event) {
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index a854e36..381580b 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -13,7 +13,7 @@
 jjaggi@google.com
 kwekua@google.com
 roosa@google.com
-per-file package_item_info.proto = toddke@google.com,patb@google.com
+per-file package_item_info.proto = file:/PACKAGE_MANAGER_OWNERS
 per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS
 per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
 per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 48243f2..5fc2a59 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -207,6 +207,7 @@
         optional SettingProto pointer_speed = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto right_click_zone = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto tap_to_click = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto tap_dragging = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Touchpad touchpad = 36;
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c4b5d81..0171f58 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3751,6 +3751,13 @@
     <permission android:name="android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS"
                 android:protectionLevel="internal|role" />
 
+    <!-- Allows an application to manage policy related to content protection.
+        <p>Protection level: internal|role
+        @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled")
+    -->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION"
+                android:protectionLevel="internal|role" />
+
     <!-- Allows an application to set device policies outside the current user
         that are critical for securing data within the current user.
         <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_*
diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml
index fe12f6e..f3acab0 100644
--- a/core/res/res/drawable-nodpi/platlogo.xml
+++ b/core/res/res/drawable-nodpi/platlogo.xml
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2024 The Android Open Source Project
+Copyright (C) 2021 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.
@@ -20,103 +20,179 @@
     android:viewportWidth="512"
     android:viewportHeight="512">
   <path
-      android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z"
-      android:strokeWidth="0">
+      android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0">
     <aapt:attr name="android:fillColor">
       <gradient 
-          android:startX="56.22"
-          android:startY="256"
-          android:endX="456.22"
-          android:endY="256"
+          android:startX="256"
+          android:startY="21.81"
+          android:endX="256"
+          android:endY="350.42"
           android:type="linear">
         <item android:offset="0" android:color="#FF073042"/>
         <item android:offset="1" android:color="#FF073042"/>
       </gradient>
     </aapt:attr>
   </path>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M171.92,216.82h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M369.04,337.63h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M330.82,273.31h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M220.14,238.94h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M293.34,349.25h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M161.05,254.24h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M378.92,192h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M137.87,323.7h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
   <path
-      android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M159.03,176.91h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M373.41,158.93h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M112.1,129.34h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M330.82,263.31h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M224.18,216.82h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M293.34,339.25h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M165.09,236.28h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M378.92,192h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M204.28,314.86h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z"
-      android:strokeWidth="55"
+      android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"
+      android:strokeWidth="56.561"
       android:fillColor="#00000000"
-      android:strokeColor="#f86733"/>
+      android:strokeColor="#f86734"/>
+  <path
+      android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z"
+      android:fillColor="#3ddc84"/>
+  <path
+      android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z"
+      android:fillColor="#fff"/>
 </vector>
+
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index ebb0d34..238f242 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -283,6 +283,20 @@
          when there's no network connection. If the scan doesn't timeout, use zero -->
     <integer name="config_radioScanningTimeout">0</integer>
 
+    <!-- The duration (in milliseconds) that the vibrator control service will wait for new
+         vibration params. -->
+    <integer name="config_requestVibrationParamsTimeout">50</integer>
+
+    <!-- Array containing the usages that should request vibration params before they are played.
+         These usages don't have strong latency requirements, e.g. ringtone and notification, and
+         can be slightly delayed. -->
+    <integer-array name="config_requestVibrationParamsForUsages">
+        <item>17</item> <!-- USAGE_ALARM -->
+        <item>33</item> <!-- USAGE_RINGTONE -->
+        <item>49</item> <!-- USAGE_NOTIFICATION -->
+        <item>65</item> <!-- USAGE_COMMUNICATION_REQUEST -->
+    </integer-array>
+
     <!-- XXXXX NOTE THE FOLLOWING RESOURCES USE THE WRONG NAMING CONVENTION.
          Please don't copy them, copy anything else. -->
 
@@ -3327,9 +3341,27 @@
     <string name="config_carrierAppInstallDialogComponent" translatable="false"
             >com.android.simappdialog/com.android.simappdialog.InstallCarrierAppActivity</string>
 
-    <!-- Name of the dialog that is used to get or save an app credential -->
+    <!-- Name of the default framework dialog that is used to get or save an app credential.
+
+    This UI should be always launch-able and is used as a fallback when an oem replacement activity
+    (defined at config_oemCredentialManagerDialogComponent) is undefined / not found. -->
     <string name="config_credentialManagerDialogComponent" translatable="false"
             >com.android.credentialmanager/com.android.credentialmanager.CredentialSelectorActivity</string>
+    <!-- Whether to allow the credential selector activity to be replaced by an activity at
+     run-time (restricted to the privileged activity specified by
+     config_credentialSelectorActivityName).
+
+     When disabled, the fallback activity defined at
+     config_credentialManagerDialogComponent will be used instead. -->
+    <bool name="config_enableOemCredentialManagerDialogComponent" translatable="false">true</bool>
+    <!-- Fully qualified activity name providing the credential selector UI, that serves the
+     CredentialManager APIs.
+
+     Used only when config_enableOemCredentialManagerDialogComponent is true.
+
+     If the activity specified cannot be found or launched, then the fallback activity defined at
+     config_credentialManagerDialogComponent will be used instead. -->
+    <string name="config_oemCredentialManagerDialogComponent" translatable="false"></string>
 
     <!-- Name of the broadcast receiver that is used to receive provider change events -->
     <string name="config_credentialManagerReceiverComponent" translatable="false"
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index dc2f74b..8b74cbf 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2080,6 +2080,8 @@
   <java-symbol type="bool" name="config_ignoreVibrationsOnWirelessCharger" />
   <java-symbol type="integer" name="config_vibrationWaveformRampDownDuration" />
   <java-symbol type="integer" name="config_radioScanningTimeout" />
+  <java-symbol type="integer" name="config_requestVibrationParamsTimeout" />
+  <java-symbol type="array" name="config_requestVibrationParamsForUsages" />
   <java-symbol type="integer" name="config_screenBrightnessSettingMinimum" />
   <java-symbol type="integer" name="config_screenBrightnessSettingMaximum" />
   <java-symbol type="integer" name="config_screenBrightnessSettingDefault" />
@@ -2281,6 +2283,8 @@
   <java-symbol type="string" name="config_platformVpnConfirmDialogComponent" />
   <java-symbol type="string" name="config_carrierAppInstallDialogComponent" />
   <java-symbol type="string" name="config_credentialManagerDialogComponent" />
+  <java-symbol type="bool" name="config_enableOemCredentialManagerDialogComponent" />
+  <java-symbol type="string" name="config_oemCredentialManagerDialogComponent" />
   <java-symbol type="string" name="config_credentialManagerReceiverComponent" />
   <java-symbol type="string" name="config_defaultNetworkScorerPackageName" />
   <java-symbol type="string" name="config_persistentDataPackageName" />
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
index 63de759..ddf3615 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
@@ -20,8 +20,6 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.graphics.Bitmap;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -88,12 +86,6 @@
             return 0;
         }
 
-        @Nullable
-        @Override
-        public Bitmap getMetadataImage(int id) {
-            return null;
-        }
-
         @Override
         public boolean startBackgroundScan() {
             return false;
@@ -138,6 +130,16 @@
     }
 
     @Test
+    public void getMetadataImage_forRadioTuner_throwsException() {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> DEFAULT_RADIO_TUNER.getMetadataImage(/* id= */ 1));
+
+        assertWithMessage("Exception for getting metadata image from default radio tuner")
+                .that(thrown).hasMessageThat()
+                .contains("Getting metadata image must be implemented in child classes");
+    }
+
+    @Test
     public void getDynamicProgramList_forRadioTuner_returnsNull() {
         assertWithMessage("Dynamic program list obtained from default radio tuner")
                 .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
index fddfd39..b3a0aba 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
@@ -290,13 +290,29 @@
     }
 
     @Test
-    public void getBitmapId_withIllegalKey() {
+    public void getBitmapId_withIllegalKey_fails() {
         String illegalKey = RadioMetadata.METADATA_KEY_ARTIST;
         RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE)
                 .build();
 
-        mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey)
-                .that(metadata.getBitmapId(illegalKey)).isEqualTo(0);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            metadata.getBitmapId(illegalKey);
+        });
+
+        mExpect.withMessage("Exception for getting string array for non-bitmap-id type key %s",
+                illegalKey).that(thrown).hasMessageThat().contains("bitmap key");
+    }
+
+    @Test
+    public void getBitmapId_withNullKey_fails() {
+        RadioMetadata metadata = mBuilder.build();
+
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            metadata.getBitmapId(/* key= */ null);
+        });
+
+        mExpect.withMessage("Exception for getting bitmap identifier with null key")
+                .that(thrown).hasMessageThat().contains("can not be null");
     }
 
     @Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index 4cda26d..5aace81 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -514,6 +514,26 @@
     }
 
     @Test
+    public void getMetadataImage_withImageIdUnavailable_fails() throws Exception {
+        int nonExistImageId = 2;
+        when(mTunerMock.getImage(nonExistImageId)).thenReturn(null);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(nonExistImageId));
+
+        assertWithMessage("Exception for getting metadata image with non-existing id")
+                .that(thrown).hasMessageThat().contains("is not available");
+    }
+
+    @Test
+    public void getMetadataImage_withInvalidId_fails() {
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(/* id= */ 0));
+
+        assertWithMessage("Exception for getting metadata image for id 0").that(thrown)
+                .hasMessageThat().contains("Invalid metadata image id 0");
+    }
+
+    @Test
     public void getMetadataImage_whenServiceDied_fails() throws Exception {
         when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException());
 
diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
index 9e8e2d6..cd5deb6 100644
--- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
@@ -20,24 +20,33 @@
 
 import static org.mockito.Mockito.when;
 
+import android.app.IBackupAgent;
 import android.app.backup.BackupAgent.IncludeExcludeRules;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.BackupAnnotations.OperationType;
 import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
+import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.backup.Flags;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -49,9 +58,12 @@
     private static final UserHandle USER_HANDLE = new UserHandle(15);
     private static final String DATA_TYPE_BACKED_UP = "test data type";
 
+    @Mock IBackupManager mIBackupManager;
     @Mock FullBackup.BackupScheme mBackupScheme;
+    @Mock Context mContext;
 
-    private BackupAgent mBackupAgent;
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Before
     public void setUp() {
@@ -67,11 +79,11 @@
         excludePaths.add(path);
         IncludeExcludeRules expectedRules = new IncludeExcludeRules(includePaths, excludePaths);
 
-        mBackupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
+        BackupAgent backupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
         when(mBackupScheme.maybeParseAndGetCanonicalExcludePaths()).thenReturn(excludePaths);
         when(mBackupScheme.maybeParseAndGetCanonicalIncludePaths()).thenReturn(includePaths);
 
-        IncludeExcludeRules rules = mBackupAgent.getIncludeExcludeRules(mBackupScheme);
+        IncludeExcludeRules rules = backupAgent.getIncludeExcludeRules(mBackupScheme);
         assertThat(rules).isEqualTo(expectedRules);
     }
 
@@ -137,6 +149,31 @@
                 0).getSuccessCount()).isEqualTo(1);
     }
 
+    @Test
+    public void doRestoreFile_agentOverrideIgnoresFile_consumesAllBytesInBuffer() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CLEAR_PIPE_AFTER_RESTORE_FILE);
+        BackupAgent agent = new TestRestoreIgnoringFullBackupAgent();
+        agent.attach(mContext);
+        agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.RESTORE);
+        IBackupAgent agentBinder = (IBackupAgent) agent.onBind();
+
+        ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+        FileOutputStream writeSide = new FileOutputStream(
+                pipes[1].getFileDescriptor());
+        writeSide.write("Hello".getBytes(StandardCharsets.UTF_8));
+
+        agentBinder.doRestoreFile(pipes[0], /* length= */ 5, BackupAgent.TYPE_FILE,
+                FullBackup.FILES_TREE_TOKEN, /* path= */ "hello_file", /* mode= */
+                0666, /* mtime= */ 12345, /* token= */ 6789, mIBackupManager);
+
+        try (FileInputStream in = new FileInputStream(pipes[0].getFileDescriptor())) {
+            assertThat(in.available()).isEqualTo(0);
+        } finally {
+            pipes[0].close();
+            pipes[1].close();
+        }
+    }
+
     private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) {
         BackupAgent agent = new TestFullBackupAgent();
         agent.onCreate(USER_HANDLE, backupDestination);
@@ -144,7 +181,6 @@
     }
 
     private static class TestFullBackupAgent extends BackupAgent {
-
         @Override
         public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
                 ParcelFileDescriptor newState) throws IOException {
@@ -162,4 +198,14 @@
             // Left empty as this is a full backup agent.
         }
     }
+
+    private static class TestRestoreIgnoringFullBackupAgent extends TestFullBackupAgent {
+
+        @Override
+        protected void onRestoreFile(ParcelFileDescriptor data, long size,
+                int type, String domain, String path, long mode, long mtime)
+                throws IOException {
+            // Ignore the file and don't consume any data.
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java
index 4dd5889..5f96c17 100644
--- a/core/tests/coretests/src/android/graphics/FontListParserTest.java
+++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java
@@ -22,14 +22,18 @@
 import static android.text.FontConfig.FontFamily.VARIANT_COMPACT;
 import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT;
 import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.fail;
 
 import android.graphics.fonts.FontCustomizationParser;
-import android.graphics.fonts.FontFamily;
 import android.graphics.fonts.FontStyle;
+import android.graphics.fonts.SystemFonts;
 import android.os.LocaleList;
 import android.text.FontConfig;
 import android.util.Xml;
@@ -64,9 +68,9 @@
         FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
                 Collections.singletonList(new FontConfig.FontFamily(
                     Arrays.asList(new FontConfig.Font(new File("test.ttf"), null, "test",
-                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null)),
-                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
-                        FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+                            new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+                            FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
         FontConfig.NamedFamilyList family = readNamedFamily(xml);
         assertThat(family).isEqualTo(expected);
     }
@@ -82,12 +86,11 @@
                 Arrays.asList(
                         new FontConfig.Font(new File("test.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "", null),
+                                0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE),
                         new FontConfig.Font(new File("test.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "", "serif")),
-                LocaleList.forLanguageTags("en"), VARIANT_DEFAULT,
-                FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                                0, "", "serif", FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                LocaleList.forLanguageTags("en"), VARIANT_DEFAULT);
 
         FontConfig.FontFamily family = readFamily(xml);
         assertThat(family).isEqualTo(expected);
@@ -103,9 +106,8 @@
                 Arrays.asList(
                         new FontConfig.Font(new File("test.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "", null)),
-                LocaleList.forLanguageTags("en"), VARIANT_COMPACT,
-                FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                                0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                LocaleList.forLanguageTags("en"), VARIANT_COMPACT);
 
         FontConfig.FontFamily family = readFamily(xml);
         assertThat(family).isEqualTo(expected);
@@ -121,9 +123,8 @@
                 Arrays.asList(
                         new FontConfig.Font(new File("test.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "", null)),
-                LocaleList.forLanguageTags("en"), VARIANT_ELEGANT,
-                FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                                0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                LocaleList.forLanguageTags("en"), VARIANT_ELEGANT);
 
         FontConfig.FontFamily family = readFamily(xml);
         assertThat(family).isEqualTo(expected);
@@ -140,13 +141,15 @@
         FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
                 Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
                       new FontConfig.Font(new File("normal.ttf"), null, "test",
-                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null),
+                            new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+                              FontConfig.Font.VAR_TYPE_AXES_NONE),
                       new FontConfig.Font(new File("weight.ttf"), null, "test",
-                        new FontStyle(100, FONT_SLANT_UPRIGHT), 0, "", null),
+                            new FontStyle(100, FONT_SLANT_UPRIGHT), 0, "", null,
+                              FontConfig.Font.VAR_TYPE_AXES_NONE),
                       new FontConfig.Font(new File("italic.ttf"), null, "test",
-                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), 0, "", null)),
-                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
-                        FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+                            new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), 0, "", null,
+                              FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
         FontConfig.NamedFamilyList family = readNamedFamily(xml);
         assertThat(family).isEqualTo(expected);
     }
@@ -168,12 +171,13 @@
                 Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
                         new FontConfig.Font(new File("test-VF.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "'wdth' 100.0,'wght' 200.0", null),
+                                0, "'wdth' 100.0,'wght' 200.0", null,
+                                FontConfig.Font.VAR_TYPE_AXES_NONE),
                         new FontConfig.Font(new File("test-VF.ttf"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "'wdth' 400.0,'wght' 700.0", null)),
-                        LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
-                        FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)),
+                                0, "'wdth' 400.0,'wght' 700.0", null,
+                                FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                        LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)),
                 "sans-serif");
         FontConfig.NamedFamilyList family = readNamedFamily(xml);
         assertThat(family).isEqualTo(expected);
@@ -190,12 +194,11 @@
                 Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
                         new FontConfig.Font(new File("test.ttc"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                0, "", null),
+                                0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE),
                         new FontConfig.Font(new File("test.ttc"), null, "test",
                                 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
-                                1, "", null)),
-                                LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
-                        FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)),
+                                1, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                                LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)),
                 "sans-serif");
         FontConfig.NamedFamilyList family = readNamedFamily(xml);
         assertThat(family).isEqualTo(expected);
@@ -211,11 +214,12 @@
         FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
                 Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
                       new FontConfig.Font(new File("test.ttc"), null, "foo",
-                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null),
+                            new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+                              FontConfig.Font.VAR_TYPE_AXES_NONE),
                       new FontConfig.Font(new File("test.ttc"), null, "test",
-                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null)),
-                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
-                        FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+                            new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null,
+                              FontConfig.Font.VAR_TYPE_AXES_NONE)),
+                    LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
         FontConfig.NamedFamilyList family = readNamedFamily(xml);
         assertThat(family).isEqualTo(expected);
     }
@@ -385,14 +389,81 @@
     public void varFamilyType() throws Exception {
         String xml = "<?xml version='1.0' encoding='UTF-8'?>"
                 + "<familyset>"
-                + "  <family name='sans-serif' varFamilyType='1'>"
-                + "    <font>test.ttf</font>"
+                + "  <family name='sans-serif'>"
+                + "    <font supportedAxes='wght'>test.ttf</font>"
+                + "    <font supportedAxes='ital'>test.ttf</font>"
+                + "    <font supportedAxes='wght,ital'>test.ttf</font>"
                 + "  </family>"
                 + "</familyset>";
         FontConfig config = readFamilies(xml, true /* include non-existing font files */);
         List<FontConfig.FontFamily> families = config.getFontFamilies();
         assertThat(families.size()).isEqualTo(1);  // legacy one should be ignored.
-        assertThat(families.get(0).getVariableFontFamilyType()).isEqualTo(1);
+        assertThat(families.get(0).getFontList().get(0).getVarTypeAxes())
+                .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_WGHT);
+        assertThat(families.get(0).getFontList().get(1).getVarTypeAxes())
+                .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_ITAL);
+        assertThat(families.get(0).getFontList().get(2).getVarTypeAxes())
+                .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_WGHT | FontConfig.Font.VAR_TYPE_AXES_ITAL);
+    }
+
+    @Test
+    public void varFamilyTypeRsolve() throws Exception {
+        String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+                + "<familyset>"
+                + "  <family name='sans-serif'>"
+                + "    <font style='normal' supportedAxes='wght'>test.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font style='normal' supportedAxes='wght'>test.ttf</font>"
+                + "    <font style='italic' supportedAxes='wght'>test.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font supportedAxes='wght,ital'>test.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font supportedAxes='ital'>test.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font>test.ttf</font>"
+                + "  </family>"
+                + "</familyset>";
+        FontConfig config = readFamilies(xml, true /* include non-existing font files */);
+        List<FontConfig.FontFamily> families = config.getFontFamilies();
+        assertThat(families.size()).isEqualTo(5);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(0), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(1), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(2), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(3), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_NONE);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(4), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_NONE);
+    }
+
+    @Test
+    public void varFamilyTypeRsolve_ForName() throws Exception {
+        String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+                + "<familyset>"
+                + "  <family name='sans-serif'>"
+                + "    <font style='normal' supportedAxes='wght'>test.ttf</font>"
+                + "  </family>"
+                + "  <family name='serif'>"
+                + "    <font style='normal' supportedAxes='wght'>test.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font style='normal' supportedAxes='wght'>test.ttf</font>"
+                + "    <font fallbackFor='serif' supportedAxes='wght,ital'>test.ttf</font>"
+                + "  </family>"
+                + "</familyset>";
+        FontConfig config = readFamilies(xml, true /* include non-existing font files */);
+        List<FontConfig.FontFamily> families = config.getFontFamilies();
+        assertThat(families.size()).isEqualTo(2);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(1), null))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY);
+        assertThat(SystemFonts.resolveVarFamilyType(families.get(1), "serif"))
+                .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL);
     }
 
     private FontConfig readFamilies(String xml, boolean allowNonExisting)
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index e7b5dff6..93c2e0e 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
@@ -121,6 +122,14 @@
     }
 
     @Test
+    public void testEmpty() throws Exception {
+        assertNotNull(Bundle.EMPTY);
+        assertEquals(0, Bundle.EMPTY.size());
+
+        new Bundle(Bundle.EMPTY);
+    }
+
+    @Test
     @IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class)
     public void testCreateFromParcel() throws Exception {
         boolean withFd;
diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
new file mode 100644
index 0000000..5959444
--- /dev/null
+++ b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class TestLooperManagerTest {
+    private static final String TAG = "TestLooperManagerTest";
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    @Test
+    public void testMainThread() throws Exception {
+        doTest(Looper.getMainLooper());
+    }
+
+    @Test
+    public void testCustomThread() throws Exception {
+        final HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        doTest(thread.getLooper());
+    }
+
+    private void doTest(Looper looper) throws Exception {
+        final TestLooperManager tlm =
+                InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper);
+
+        final Handler handler = new Handler(looper);
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        assertFalse(tlm.hasMessages(handler, null, 42));
+
+        handler.sendEmptyMessage(42);
+        handler.post(() -> {
+            latch.countDown();
+        });
+        assertTrue(tlm.hasMessages(handler, null, 42));
+        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+
+        final Message first = tlm.next();
+        assertEquals(42, first.what);
+        assertNull(first.callback);
+        tlm.execute(first);
+        assertFalse(tlm.hasMessages(handler, null, 42));
+        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+        tlm.recycle(first);
+
+        final Message second = tlm.next();
+        assertNotNull(second.callback);
+        tlm.execute(second);
+        assertFalse(tlm.hasMessages(handler, null, 42));
+        assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
+        tlm.recycle(second);
+
+        tlm.release();
+    }
+}
diff --git a/core/tests/coretests/src/android/util/LogTest.java b/core/tests/coretests/src/android/util/LogTest.java
index f9966a1..15caac9 100644
--- a/core/tests/coretests/src/android/util/LogTest.java
+++ b/core/tests/coretests/src/android/util/LogTest.java
@@ -37,6 +37,13 @@
     private static final String LOG_TAG = "LogTest";
 
     @Test
+    public void testWtf() {
+        Log.wtf(LOG_TAG, "Message");
+        Log.wtf(LOG_TAG, "Message", new Throwable("Throwable"));
+        Log.wtf(LOG_TAG, new Throwable("Throwable"));
+    }
+
+    @Test
     @Ignore
     public void testIsLoggable() {
         // First clear any SystemProperty setting for our test key.
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 543d73b..5862711 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -450,13 +450,8 @@
     }
 
     private RemoteViews.DrawInstructions getDrawInstructions() {
-        final byte[] first = new byte[] {'f', 'i', 'r', 's', 't'};
-        final byte[] second = new byte[] {'s', 'e', 'c', 'o', 'n', 'd'};
-        final RemoteViews.DrawInstructions drawInstructions =
-                new RemoteViews.DrawInstructions.Builder(
-                        Collections.singletonList(first)).build();
-        drawInstructions.appendInstructions(second);
-        return drawInstructions;
+        final byte[] bytes = new byte[] {'h', 'e', 'l', 'l', 'o'};
+        return new RemoteViews.DrawInstructions.Builder(Collections.singletonList(bytes)).build();
     }
 
     private RemoteViews createViewChained(int depth, String... texts) {
diff --git a/core/tests/utiltests/src/android/util/SlogTest.java b/core/tests/utiltests/src/android/util/SlogTest.java
index 6f761e3..738c668 100644
--- a/core/tests/utiltests/src/android/util/SlogTest.java
+++ b/core/tests/utiltests/src/android/util/SlogTest.java
@@ -44,4 +44,11 @@
         Slog.w(TAG, MSG, THROWABLE);
         Slog.e(TAG, MSG, THROWABLE);
     }
+
+    @Test
+    public void testWtf() {
+        Slog.wtf(TAG, MSG);
+        Slog.wtf(TAG, MSG, THROWABLE);
+        Slog.wtf(TAG, THROWABLE);
+    }
 }
diff --git a/data/etc/OWNERS b/data/etc/OWNERS
index ea23aba..245f216 100644
--- a/data/etc/OWNERS
+++ b/data/etc/OWNERS
@@ -1,3 +1,5 @@
+include /PACKAGE_MANAGER_OWNERS
+
 alanstokes@google.com
 cbrubaker@google.com
 hackbod@android.com
@@ -6,11 +8,6 @@
 jsharkey@android.com
 jsharkey@google.com
 lorenzo@google.com
-svetoslavganov@android.com
-svetoslavganov@google.com
-toddke@android.com
-toddke@google.com
-patb@google.com
 yamasani@google.com
 
 per-file preinstalled-packages* = file:/MULTIUSER_OWNERS
diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml
index 1bd182f..15ea15a 100644
--- a/data/fonts/font_fallback.xml
+++ b/data/fonts/font_fallback.xml
@@ -11,12 +11,84 @@
     effectively add 300 to the weight, this ensures that 900 is the bold
     paired with the 500 weight, ensuring adequate contrast.
 
-    TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required
+
+    The font_fallback.xml defines the list of font used by the system.
+
+    `familyset` node:
+      A `familyset` element must be a root node of the font_fallback.xml. No attributes are allowed
+      to `familyset` node.
+      The `familyset` node can contains `family` and `alias` nodes. Any other nodes will be ignored.
+
+    `family` node:
+      A `family` node defines a single font family definition.
+      A font family is a set of fonts for drawing text in various styles such as weight, slant.
+      There are three types of families, default family, named family and locale fallback family.
+
+      The default family is a special family node appeared the first node of the `familyset` node.
+      The default family is used as first priority fallback.
+      Only `name` attribute can be used for default family node. If the `name` attribute is
+      specified, This family will also works as named family.
+
+      The named family is a family that has name attribute. The named family defines a new fallback.
+      For example, if the name attribute is "serif", it creates serif fallback. Developers can
+      access the fallback by using Typeface#create API.
+      The named family can not have attribute other than `name` attribute. The `name` attribute
+      cannot be empty.
+
+      The locale fallback family is a font family that is used for fallback. The fallback family is
+      used when the named family or default family cannot be used. The locale fallback family can
+      have `lang` attribute and `variant` attribute. The `lang` attribute is an optional comma
+      separated BCP-47i language tag. The `variant` is an optional attribute that can be one one
+      `element`, `compact`. If a `variant` attribute is not specified, it is treated as default.
+
+    `alias` node:
+      An `alias` node defines a alias of named family with changing weight offset. An `alias` node
+      can have mandatory `name` and `to` attribute and optional `weight` attribute. This `alias`
+      defines new fallback that has the name of specified `name` attribute. The fallback list is
+      the same to the fallback that of the name specified with `to` attribute. If `weight` attribute
+      is specified, the base weight offset is shifted to the specified value. For example, if the
+      `weight` is 500, the output text is drawn with 500 of weight.
+
+    `font` node:
+      A `font` node defines a single font definition. There are two types of fonts, static font and
+      variable font.
+
+      A static font can have `weight`, `style`, `index` and `postScriptName` attributes. A `weight`
+      is a mandatory attribute that defines the weight of the font. Any number between 0 to 1000 is
+      valid. A `style` is a mandatory attribute that defines the style of the font. A 'style'
+      attribute can be `normal` or `italic`. An `index` is an optional attribute that defines the
+      index of the font collection. If this is not specified, it is treated as 0. If the font file
+      is not a font collection, this attribute is ignored. A `postScriptName` attribute is an
+      optional attribute. A PostScript name is used for identifying target of system font update.
+      If this is not specified, the system assumes the filename is same to PostScript name of the
+      font file. For example, if the font file is "Roboto-Regular.ttf", the system assume the
+      PostScript name of this font is "Roboto-Regular".
+
+      A variable font can be only defined for the variable font file. A variable font can have
+      `axis` child nodes for specifying axis values. A variable font can have all attribute of
+      static font and can have additional `supportedAxes` attribute. A `supportedAxes` attribute
+      is a comma separated supported axis tags. As of Android V, only `wght` and `ital` axes can
+      be specified.
+
+      If `supportedAxes` attribute is not specified, this `font` node works as static font of the
+      single instance of variable font specified with `axis` children.
+
+      If `supportedAxes` attribute is specified, the system dynamically create font instance for the
+      given weight and style value. If `wght` is specified in `supportedAxes` attribute the `weight`
+      attribute and `axis` child that has `wght` tag become optional and ignored because it is
+      determined by system at runtime. Similarly, if `ital` is specified in `supportedAxes`
+      attribute, the `style` attribute and `axis` child that has `ital` tag become optional and
+      ignored.
+
+    `axis` node:
+      An `axis` node defines a font variation value for a tag. An `axis` node can have two mandatory
+      attributes, `tag` and `value`. If the font is variable font and the same tag `axis` node is
+      specified in `supportedAxes` attribute, the style value works like a default instance.
 -->
-<familyset version="23">
+<familyset>
     <!-- first font is default -->
-    <family name="sans-serif" varFamilyType="2">
-        <font>Roboto-Regular.ttf
+    <family name="sans-serif">
+        <font supportedAxes="wght,ital">Roboto-Regular.ttf
           <axis tag="wdth" stylevalue="100" />
         </font>
    </family>
@@ -32,8 +104,8 @@
     <alias name="tahoma" to="sans-serif" />
     <alias name="verdana" to="sans-serif" />
 
-    <family name="sans-serif-condensed" varFamilyType="2">
-      <font>Roboto-Regular.ttf
+    <family name="sans-serif-condensed">
+      <font supportedAxes="wght,ital">Roboto-Regular.ttf
         <axis tag="wdth" stylevalue="75" />
       </font>
     </family>
@@ -72,8 +144,8 @@
         <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font>
     </family>
 
-    <family name="cursive" varFamilyType="1">
-      <font>DancingScript-Regular.ttf</font>
+    <family name="cursive">
+      <font supportedAxes="wght">DancingScript-Regular.ttf</font>
     </family>
 
     <family name="sans-serif-smallcaps">
@@ -90,8 +162,8 @@
     </family>
     <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/>
 
-    <family name="roboto-flex" varFamilyType="2">
-        <font>RobotoFlex-Regular.ttf
+    <family name="roboto-flex">
+        <font supportedAxes="wght">RobotoFlex-Regular.ttf
           <axis tag="wdth" stylevalue="100" />
         </font>
     </family>
@@ -109,11 +181,11 @@
         </font>
         <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font>
     </family>
-    <family lang="und-Ethi" varFamilyType="1">
-        <font postScriptName="NotoSansEthiopic-Regular">
+    <family lang="und-Ethi">
+        <font postScriptName="NotoSansEthiopic-Regular" supportedAxes="wght">
             NotoSansEthiopic-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular" supportedAxes="wght">
             NotoSerifEthiopic-VF.ttf
         </font>
     </family>
@@ -140,32 +212,32 @@
         </font>
         <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font>
     </family>
-    <family lang="und-Armn" varFamilyType="1">
-        <font postScriptName="NotoSansArmenian-Regular">
+    <family lang="und-Armn">
+        <font postScriptName="NotoSansArmenian-Regular" supportedAxes="wght">
             NotoSansArmenian-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular" supportedAxes="wght">
             NotoSerifArmenian-VF.ttf
         </font>
     </family>
-    <family lang="und-Geor,und-Geok" varFamilyType="1">
-        <font postScriptName="NotoSansGeorgian-Regular">
+    <family lang="und-Geor,und-Geok">
+        <font postScriptName="NotoSansGeorgian-Regular" supportedAxes="wght">
             NotoSansGeorgian-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular" supportedAxes="wght">
             NotoSerifGeorgian-VF.ttf
         </font>
     </family>
-    <family lang="und-Deva" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansDevanagari-Regular">
+    <family lang="und-Deva" variant="elegant">
+        <font postScriptName="NotoSansDevanagari-Regular" supportedAxes="wght">
             NotoSansDevanagari-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular" supportedAxes="wght">
             NotoSerifDevanagari-VF.ttf
         </font>
     </family>
-    <family lang="und-Deva" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansDevanagariUI-Regular">
+    <family lang="und-Deva" variant="compact">
+        <font postScriptName="NotoSansDevanagariUI-Regular" supportedAxes="wght">
             NotoSansDevanagariUI-VF.ttf
         </font>
     </family>
@@ -178,21 +250,9 @@
             NotoSansGujarati-Regular.ttf
         </font>
         <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font>
-        <font weight="400" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="700"/>
+        <font style="normal" fallbackFor="serif" postScriptName="NotoSerifGujarati-Regular"
+          supportedAxes="wght">
+          NotoSerifGujarati-VF.ttf
         </font>
     </family>
     <family lang="und-Gujr" variant="compact">
@@ -201,81 +261,81 @@
         </font>
         <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font>
     </family>
-    <family lang="und-Guru" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansGurmukhi-Regular">
+    <family lang="und-Guru" variant="elegant">
+        <font postScriptName="NotoSansGurmukhi-Regular" supportedAxes="wght">
             NotoSansGurmukhi-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular" supportedAxes="wght">
             NotoSerifGurmukhi-VF.ttf
         </font>
     </family>
-    <family lang="und-Guru" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansGurmukhiUI-Regular">
+    <family lang="und-Guru" variant="compact">
+        <font postScriptName="NotoSansGurmukhiUI-Regular" supportedAxes="wght">
             NotoSansGurmukhiUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Taml" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansTamil-Regular">
+    <family lang="und-Taml" variant="elegant">
+        <font postScriptName="NotoSansTamil-Regular" supportedAxes="wght">
             NotoSansTamil-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular" supportedAxes="wght">
             NotoSerifTamil-VF.ttf
         </font>
     </family>
-    <family lang="und-Taml" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansTamilUI-Regular">
+    <family lang="und-Taml" variant="compact">
+        <font postScriptName="NotoSansTamilUI-Regular" supportedAxes="wght">
             NotoSansTamilUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Mlym" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansMalayalam-Regular">
+    <family lang="und-Mlym" variant="elegant">
+        <font postScriptName="NotoSansMalayalam-Regular" supportedAxes="wght">
             NotoSansMalayalam-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular" supportedAxes="wght">
             NotoSerifMalayalam-VF.ttf
         </font>
     </family>
-    <family lang="und-Mlym" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansMalayalamUI-Regular">
+    <family lang="und-Mlym" variant="compact">
+        <font postScriptName="NotoSansMalayalamUI-Regular" supportedAxes="wght">
             NotoSansMalayalamUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Beng" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansBengali-Regular">
+    <family lang="und-Beng" variant="elegant">
+        <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
             NotoSansBengali-VF.ttf
         </font>
         <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
             NotoSerifBengali-VF.ttf
         </font>
     </family>
-    <family lang="und-Beng" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansBengaliUI-Regular">
+    <family lang="und-Beng" variant="compact">
+        <font postScriptName="NotoSansBengaliUI-Regular" supportedAxes="wght">
             NotoSansBengaliUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Telu" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansTelugu-Regular">
+    <family lang="und-Telu" variant="elegant">
+        <font postScriptName="NotoSansTelugu-Regular" supportedAxes="wght">
             NotoSansTelugu-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular" supportedAxes="wght">
             NotoSerifTelugu-VF.ttf
         </font>
     </family>
-    <family lang="und-Telu" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansTeluguUI-Regular">
+    <family lang="und-Telu" variant="compact">
+        <font postScriptName="NotoSansTeluguUI-Regular" supportedAxes="wght">
             NotoSansTeluguUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Knda" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansKannada-Regular">
+    <family lang="und-Knda" variant="elegant">
+        <font postScriptName="NotoSansKannada-Regular" supportedAxes="wght">
             NotoSansKannada-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular" supportedAxes="wght">
             NotoSerifKannada-VF.ttf
         </font>
     </family>
-    <family lang="und-Knda" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansKannadaUI-Regular">
+    <family lang="und-Knda" variant="compact">
+        <font postScriptName="NotoSansKannadaUI-Regular" supportedAxes="wght">
             NotoSansKannadaUI-VF.ttf
         </font>
     </family>
@@ -290,19 +350,20 @@
         </font>
         <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font>
     </family>
-    <family lang="und-Sinh" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansSinhala-Regular">
+    <family lang="und-Sinh" variant="elegant">
+        <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
             NotoSansSinhala-VF.ttf
         </font>
         <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
             NotoSerifSinhala-VF.ttf
         </font>
     </family>
-    <family lang="und-Sinh" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansSinhalaUI-Regular">
+    <family lang="und-Sinh" variant="compact">
+        <font postScriptName="NotoSansSinhalaUI-Regular" supportedAxes="wght">
             NotoSansSinhalaUI-VF.ttf
         </font>
     </family>
+    <!-- TODO: NotoSansKhmer uses non-standard wght value, so cannot use auto-adjustment. -->
     <family lang="und-Khmr" variant="elegant">
         <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular">
             NotoSansKhmer-VF.ttf
@@ -398,8 +459,8 @@
     <family lang="und-Ahom">
         <font weight="400" style="normal">NotoSansAhom-Regular.otf</font>
     </family>
-    <family lang="und-Adlm" varFamilyType="1">
-        <font postScriptName="NotoSansAdlam-Regular">
+    <family lang="und-Adlm">
+        <font postScriptName="NotoSansAdlam-Regular" supportedAxes="wght">
             NotoSansAdlam-VF.ttf
         </font>
     </family>
@@ -686,8 +747,8 @@
             NotoSansTaiViet-Regular.ttf
         </font>
     </family>
-    <family lang="und-Tibt" varFamilyType="1">
-        <font postScriptName="NotoSerifTibetan-Regular">
+    <family lang="und-Tibt">
+        <font postScriptName="NotoSerifTibetan-Regular" supportedAxes="wght">
             NotoSerifTibetan-VF.ttf
         </font>
     </family>
@@ -865,27 +926,27 @@
     <family lang="und-Dogr">
         <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font>
     </family>
-    <family lang="und-Medf" varFamilyType="1">
-        <font postScriptName="NotoSansMedefaidrin-Regular">
+    <family lang="und-Medf">
+        <font postScriptName="NotoSansMedefaidrin-Regular" supportedAxes="wght">
             NotoSansMedefaidrin-VF.ttf
         </font>
     </family>
-    <family lang="und-Soyo" varFamilyType="1">
+    <family lang="und-Soyo" supportedAxes="wght">
         <font postScriptName="NotoSansSoyombo-Regular">
             NotoSansSoyombo-VF.ttf
         </font>
     </family>
-    <family lang="und-Takr" varFamilyType="1">
+    <family lang="und-Takr" supportedAxes="wght">
         <font postScriptName="NotoSansTakri-Regular">
             NotoSansTakri-VF.ttf
         </font>
     </family>
-    <family lang="und-Hmnp" varFamilyType="1">
+    <family lang="und-Hmnp" supportedAxes="wght">
         <font postScriptName="NotoSerifHmongNyiakeng-Regular">
             NotoSerifNyiakengPuachueHmong-VF.ttf
         </font>
     </family>
-    <family lang="und-Yezi" varFamilyType="1">
+    <family lang="und-Yezi" supportedAxes="wght">
         <font postScriptName="NotoSerifYezidi-Regular">
             NotoSerifYezidi-VF.ttf
         </font>
diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml
index 75bc74e..c1ca67e 100644
--- a/data/fonts/font_fallback_cjkvf.xml
+++ b/data/fonts/font_fallback_cjkvf.xml
@@ -11,12 +11,84 @@
     effectively add 300 to the weight, this ensures that 900 is the bold
     paired with the 500 weight, ensuring adequate contrast.
 
-    TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required
+
+    The font_fallback.xml defines the list of font used by the system.
+
+    `familyset` node:
+      A `familyset` element must be a root node of the font_fallback.xml. No attributes are allowed
+      to `familyset` node.
+      The `familyset` node can contains `family` and `alias` nodes. Any other nodes will be ignored.
+
+    `family` node:
+      A `family` node defines a single font family definition.
+      A font family is a set of fonts for drawing text in various styles such as weight, slant.
+      There are three types of families, default family, named family and locale fallback family.
+
+      The default family is a special family node appeared the first node of the `familyset` node.
+      The default family is used as first priority fallback.
+      Only `name` attribute can be used for default family node. If the `name` attribute is
+      specified, This family will also works as named family.
+
+      The named family is a family that has name attribute. The named family defines a new fallback.
+      For example, if the name attribute is "serif", it creates serif fallback. Developers can
+      access the fallback by using Typeface#create API.
+      The named family can not have attribute other than `name` attribute. The `name` attribute
+      cannot be empty.
+
+      The locale fallback family is a font family that is used for fallback. The fallback family is
+      used when the named family or default family cannot be used. The locale fallback family can
+      have `lang` attribute and `variant` attribute. The `lang` attribute is an optional comma
+      separated BCP-47i language tag. The `variant` is an optional attribute that can be one one
+      `element`, `compact`. If a `variant` attribute is not specified, it is treated as default.
+
+    `alias` node:
+      An `alias` node defines a alias of named family with changing weight offset. An `alias` node
+      can have mandatory `name` and `to` attribute and optional `weight` attribute. This `alias`
+      defines new fallback that has the name of specified `name` attribute. The fallback list is
+      the same to the fallback that of the name specified with `to` attribute. If `weight` attribute
+      is specified, the base weight offset is shifted to the specified value. For example, if the
+      `weight` is 500, the output text is drawn with 500 of weight.
+
+    `font` node:
+      A `font` node defines a single font definition. There are two types of fonts, static font and
+      variable font.
+
+      A static font can have `weight`, `style`, `index` and `postScriptName` attributes. A `weight`
+      is a mandatory attribute that defines the weight of the font. Any number between 0 to 1000 is
+      valid. A `style` is a mandatory attribute that defines the style of the font. A 'style'
+      attribute can be `normal` or `italic`. An `index` is an optional attribute that defines the
+      index of the font collection. If this is not specified, it is treated as 0. If the font file
+      is not a font collection, this attribute is ignored. A `postScriptName` attribute is an
+      optional attribute. A PostScript name is used for identifying target of system font update.
+      If this is not specified, the system assumes the filename is same to PostScript name of the
+      font file. For example, if the font file is "Roboto-Regular.ttf", the system assume the
+      PostScript name of this font is "Roboto-Regular".
+
+      A variable font can be only defined for the variable font file. A variable font can have
+      `axis` child nodes for specifying axis values. A variable font can have all attribute of
+      static font and can have additional `supportedAxes` attribute. A `supportedAxes` attribute
+      is a comma separated supported axis tags. As of Android V, only `wght` and `ital` axes can
+      be specified.
+
+      If `supportedAxes` attribute is not specified, this `font` node works as static font of the
+      single instance of variable font specified with `axis` children.
+
+      If `supportedAxes` attribute is specified, the system dynamically create font instance for the
+      given weight and style value. If `wght` is specified in `supportedAxes` attribute the `weight`
+      attribute and `axis` child that has `wght` tag become optional and ignored because it is
+      determined by system at runtime. Similarly, if `ital` is specified in `supportedAxes`
+      attribute, the `style` attribute and `axis` child that has `ital` tag become optional and
+      ignored.
+
+    `axis` node:
+      An `axis` node defines a font variation value for a tag. An `axis` node can have two mandatory
+      attributes, `tag` and `value`. If the font is variable font and the same tag `axis` node is
+      specified in `supportedAxes` attribute, the style value works like a default instance.
 -->
-<familyset version="23">
+<familyset>
     <!-- first font is default -->
-    <family name="sans-serif" varFamilyType="2">
-        <font>Roboto-Regular.ttf
+    <family name="sans-serif">
+        <font supportedAxes="wght,ital">Roboto-Regular.ttf
           <axis tag="wdth" stylevalue="100" />
         </font>
    </family>
@@ -32,8 +104,8 @@
     <alias name="tahoma" to="sans-serif" />
     <alias name="verdana" to="sans-serif" />
 
-    <family name="sans-serif-condensed" varFamilyType="2">
-      <font>Roboto-Regular.ttf
+    <family name="sans-serif-condensed">
+      <font supportedAxes="wght,ital">Roboto-Regular.ttf
         <axis tag="wdth" stylevalue="75" />
       </font>
     </family>
@@ -72,8 +144,8 @@
         <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font>
     </family>
 
-    <family name="cursive" varFamilyType="1">
-      <font>DancingScript-Regular.ttf</font>
+    <family name="cursive">
+      <font supportedAxes="wght">DancingScript-Regular.ttf</font>
     </family>
 
     <family name="sans-serif-smallcaps">
@@ -90,8 +162,8 @@
     </family>
     <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/>
 
-    <family name="roboto-flex" varFamilyType="2">
-        <font>RobotoFlex-Regular.ttf
+    <family name="roboto-flex">
+        <font supportedAxes="wght">RobotoFlex-Regular.ttf
           <axis tag="wdth" stylevalue="100" />
         </font>
     </family>
@@ -109,11 +181,11 @@
         </font>
         <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font>
     </family>
-    <family lang="und-Ethi" varFamilyType="1">
-        <font postScriptName="NotoSansEthiopic-Regular">
+    <family lang="und-Ethi">
+        <font postScriptName="NotoSansEthiopic-Regular" supportedAxes="wght">
             NotoSansEthiopic-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular" supportedAxes="wght">
             NotoSerifEthiopic-VF.ttf
         </font>
     </family>
@@ -140,32 +212,32 @@
         </font>
         <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font>
     </family>
-    <family lang="und-Armn" varFamilyType="1">
-        <font postScriptName="NotoSansArmenian-Regular">
+    <family lang="und-Armn">
+        <font postScriptName="NotoSansArmenian-Regular" supportedAxes="wght">
             NotoSansArmenian-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular" supportedAxes="wght">
             NotoSerifArmenian-VF.ttf
         </font>
     </family>
-    <family lang="und-Geor,und-Geok" varFamilyType="1">
-        <font postScriptName="NotoSansGeorgian-Regular">
+    <family lang="und-Geor,und-Geok">
+        <font postScriptName="NotoSansGeorgian-Regular" supportedAxes="wght">
             NotoSansGeorgian-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular" supportedAxes="wght">
             NotoSerifGeorgian-VF.ttf
         </font>
     </family>
-    <family lang="und-Deva" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansDevanagari-Regular">
+    <family lang="und-Deva" variant="elegant">
+        <font postScriptName="NotoSansDevanagari-Regular" supportedAxes="wght">
             NotoSansDevanagari-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular" supportedAxes="wght">
             NotoSerifDevanagari-VF.ttf
         </font>
     </family>
-    <family lang="und-Deva" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansDevanagariUI-Regular">
+    <family lang="und-Deva" variant="compact">
+        <font postScriptName="NotoSansDevanagariUI-Regular" supportedAxes="wght">
             NotoSansDevanagariUI-VF.ttf
         </font>
     </family>
@@ -178,21 +250,9 @@
             NotoSansGujarati-Regular.ttf
         </font>
         <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font>
-        <font weight="400" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" fallbackFor="serif"
-              postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
-            <axis tag="wght" stylevalue="700"/>
+        <font style="normal" fallbackFor="serif" postScriptName="NotoSerifGujarati-Regular"
+          supportedAxes="wght">
+          NotoSerifGujarati-VF.ttf
         </font>
     </family>
     <family lang="und-Gujr" variant="compact">
@@ -201,81 +261,81 @@
         </font>
         <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font>
     </family>
-    <family lang="und-Guru" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansGurmukhi-Regular">
+    <family lang="und-Guru" variant="elegant">
+        <font postScriptName="NotoSansGurmukhi-Regular" supportedAxes="wght">
             NotoSansGurmukhi-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular" supportedAxes="wght">
             NotoSerifGurmukhi-VF.ttf
         </font>
     </family>
-    <family lang="und-Guru" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansGurmukhiUI-Regular">
+    <family lang="und-Guru" variant="compact">
+        <font postScriptName="NotoSansGurmukhiUI-Regular" supportedAxes="wght">
             NotoSansGurmukhiUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Taml" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansTamil-Regular">
+    <family lang="und-Taml" variant="elegant">
+        <font postScriptName="NotoSansTamil-Regular" supportedAxes="wght">
             NotoSansTamil-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular" supportedAxes="wght">
             NotoSerifTamil-VF.ttf
         </font>
     </family>
-    <family lang="und-Taml" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansTamilUI-Regular">
+    <family lang="und-Taml" variant="compact">
+        <font postScriptName="NotoSansTamilUI-Regular" supportedAxes="wght">
             NotoSansTamilUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Mlym" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansMalayalam-Regular">
+    <family lang="und-Mlym" variant="elegant">
+        <font postScriptName="NotoSansMalayalam-Regular" supportedAxes="wght">
             NotoSansMalayalam-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular" supportedAxes="wght">
             NotoSerifMalayalam-VF.ttf
         </font>
     </family>
-    <family lang="und-Mlym" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansMalayalamUI-Regular">
+    <family lang="und-Mlym" variant="compact">
+        <font postScriptName="NotoSansMalayalamUI-Regular" supportedAxes="wght">
             NotoSansMalayalamUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Beng" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansBengali-Regular">
+    <family lang="und-Beng" variant="elegant">
+        <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
             NotoSansBengali-VF.ttf
         </font>
         <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
             NotoSerifBengali-VF.ttf
         </font>
     </family>
-    <family lang="und-Beng" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansBengaliUI-Regular">
+    <family lang="und-Beng" variant="compact">
+        <font postScriptName="NotoSansBengaliUI-Regular" supportedAxes="wght">
             NotoSansBengaliUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Telu" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansTelugu-Regular">
+    <family lang="und-Telu" variant="elegant">
+        <font postScriptName="NotoSansTelugu-Regular" supportedAxes="wght">
             NotoSansTelugu-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular" supportedAxes="wght">
             NotoSerifTelugu-VF.ttf
         </font>
     </family>
-    <family lang="und-Telu" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansTeluguUI-Regular">
+    <family lang="und-Telu" variant="compact">
+        <font postScriptName="NotoSansTeluguUI-Regular" supportedAxes="wght">
             NotoSansTeluguUI-VF.ttf
         </font>
     </family>
-    <family lang="und-Knda" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansKannada-Regular">
+    <family lang="und-Knda" variant="elegant">
+        <font postScriptName="NotoSansKannada-Regular" supportedAxes="wght">
             NotoSansKannada-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular" supportedAxes="wght">
             NotoSerifKannada-VF.ttf
         </font>
     </family>
-    <family lang="und-Knda" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansKannadaUI-Regular">
+    <family lang="und-Knda" variant="compact">
+        <font postScriptName="NotoSansKannadaUI-Regular" supportedAxes="wght">
             NotoSansKannadaUI-VF.ttf
         </font>
     </family>
@@ -290,19 +350,20 @@
         </font>
         <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font>
     </family>
-    <family lang="und-Sinh" variant="elegant" varFamilyType="1">
-        <font postScriptName="NotoSansSinhala-Regular">
+    <family lang="und-Sinh" variant="elegant">
+        <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
             NotoSansSinhala-VF.ttf
         </font>
         <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
             NotoSerifSinhala-VF.ttf
         </font>
     </family>
-    <family lang="und-Sinh" variant="compact" varFamilyType="1">
-        <font postScriptName="NotoSansSinhalaUI-Regular">
+    <family lang="und-Sinh" variant="compact">
+        <font postScriptName="NotoSansSinhalaUI-Regular" supportedAxes="wght">
             NotoSansSinhalaUI-VF.ttf
         </font>
     </family>
+    <!-- TODO: NotoSansKhmer uses non-standard wght value, so cannot use auto-adjustment. -->
     <family lang="und-Khmr" variant="elegant">
         <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular">
             NotoSansKhmer-VF.ttf
@@ -398,8 +459,8 @@
     <family lang="und-Ahom">
         <font weight="400" style="normal">NotoSansAhom-Regular.otf</font>
     </family>
-    <family lang="und-Adlm" varFamilyType="1">
-        <font postScriptName="NotoSansAdlam-Regular">
+    <family lang="und-Adlm">
+        <font postScriptName="NotoSansAdlam-Regular" supportedAxes="wght">
             NotoSansAdlam-VF.ttf
         </font>
     </family>
@@ -686,8 +747,8 @@
             NotoSansTaiViet-Regular.ttf
         </font>
     </family>
-    <family lang="und-Tibt" varFamilyType="1">
-        <font postScriptName="NotoSerifTibetan-Regular">
+    <family lang="und-Tibt">
+        <font postScriptName="NotoSerifTibetan-Regular" supportedAxes="wght">
             NotoSerifTibetan-VF.ttf
         </font>
     </family>
@@ -707,123 +768,36 @@
         <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font>
     </family>
     <family lang="zh-Hans">
-        <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
+        <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"
+            supportedAxes="wght">
             NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="100"/>
-        </font>
-        <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="200"/>
-        </font>
-        <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="300"/>
-        </font>
-        <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="700"/>
-        </font>
-        <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="800"/>
-        </font>
-        <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="900"/>
+            <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+                 for making regular style as default. -->
+            <axis tag="wght" stylevalue="400" />
         </font>
         <font weight="400" style="normal" index="2" fallbackFor="serif"
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
     <family lang="zh-Hant,zh-Bopo">
-        <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
+        <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"
+            supportedAxes="wght">
             NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="100"/>
-        </font>
-        <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="200"/>
-        </font>
-        <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="300"/>
-        </font>
-        <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="700"/>
-        </font>
-        <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="800"/>
-        </font>
-        <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="900"/>
+            <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+                 for making regular style as default. -->
+            <axis tag="wght" stylevalue="400" />
         </font>
         <font weight="400" style="normal" index="3" fallbackFor="serif"
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
     <family lang="ja">
-        <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
+        <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"
+            supportedAxes="wght">
             NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="100"/>
-        </font>
-        <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="200"/>
-        </font>
-        <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="300"/>
-        </font>
-        <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="700"/>
-        </font>
-        <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="800"/>
-        </font>
-        <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="900"/>
+            <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+                 for making regular style as default. -->
+            <axis tag="wght" stylevalue="400" />
         </font>
         <font weight="400" style="normal" index="0" fallbackFor="serif"
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
@@ -840,41 +814,12 @@
         </font>
     </family>
     <family lang="ko">
-        <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
+        <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"
+            supportedAxes="wght">
             NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="100"/>
-        </font>
-        <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="200"/>
-        </font>
-        <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="300"/>
-        </font>
-        <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="400"/>
-        </font>
-        <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="500"/>
-        </font>
-        <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="600"/>
-        </font>
-        <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="700"/>
-        </font>
-        <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="800"/>
-        </font>
-        <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
-            NotoSansCJK-Regular.ttc
-            <axis tag="wght" stylevalue="900"/>
+            <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+                 for making regular style as default. -->
+            <axis tag="wght" stylevalue="400" />
         </font>
         <font weight="400" style="normal" index="1" fallbackFor="serif"
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
@@ -997,27 +942,27 @@
     <family lang="und-Dogr">
         <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font>
     </family>
-    <family lang="und-Medf" varFamilyType="1">
-        <font postScriptName="NotoSansMedefaidrin-Regular">
+    <family lang="und-Medf">
+        <font postScriptName="NotoSansMedefaidrin-Regular" supportedAxes="wght">
             NotoSansMedefaidrin-VF.ttf
         </font>
     </family>
-    <family lang="und-Soyo" varFamilyType="1">
+    <family lang="und-Soyo" supportedAxes="wght">
         <font postScriptName="NotoSansSoyombo-Regular">
             NotoSansSoyombo-VF.ttf
         </font>
     </family>
-    <family lang="und-Takr" varFamilyType="1">
+    <family lang="und-Takr" supportedAxes="wght">
         <font postScriptName="NotoSansTakri-Regular">
             NotoSansTakri-VF.ttf
         </font>
     </family>
-    <family lang="und-Hmnp" varFamilyType="1">
+    <family lang="und-Hmnp" supportedAxes="wght">
         <font postScriptName="NotoSerifHmongNyiakeng-Regular">
             NotoSerifNyiakengPuachueHmong-VF.ttf
         </font>
     </family>
-    <family lang="und-Yezi" varFamilyType="1">
+    <family lang="und-Yezi" supportedAxes="wght">
         <font postScriptName="NotoSerifYezidi-Regular">
             NotoSerifYezidi-VF.ttf
         </font>
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index d1aceaf..b33a5d2 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -18,6 +18,7 @@
 
 import android.annotation.ColorInt;
 import android.annotation.ColorLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -30,6 +31,8 @@
 import android.os.Build;
 import android.text.TextShaper;
 
+import com.android.graphics.hwui.flags.Flags;
+
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
 
@@ -766,6 +769,21 @@
     }
 
     /**
+     * Preconcat the current matrix with the specified matrix. If the specified
+     * matrix is null, this method does nothing. If the canvas's matrix is changed in the z-axis
+     * through this function, the deprecated {@link #getMatrix()} method will return a 3x3 with
+     * z-axis info stripped away.
+     *
+     * @param m The 4x4 matrix to preconcatenate with the current matrix
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void concat44(@Nullable Matrix44 m) {
+        if (m != null) {
+            nConcat(mNativeCanvasWrapper, m.mBackingArray);
+        }
+    }
+
+    /**
      * Completely replace the current matrix with the specified matrix. If the
      * matrix parameter is null, then the current matrix is reset to identity.
      *
@@ -1444,6 +1462,8 @@
     private static native void nSkew(long canvasHandle, float sx, float sy);
     @CriticalNative
     private static native void nConcat(long nativeCanvas, long nativeMatrix);
+    @FastNative
+    private static native void nConcat(long nativeCanvas, float[] mat);
     @CriticalNative
     private static native void nSetMatrix(long nativeCanvas, long nativeMatrix);
     @CriticalNative
diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java
index 52b0b95..17c2dd9 100644
--- a/graphics/java/android/graphics/FontListParser.java
+++ b/graphics/java/android/graphics/FontListParser.java
@@ -16,10 +16,6 @@
 
 package android.graphics;
 
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 import static android.text.FontConfig.NamedFamilyList;
 
 import android.annotation.NonNull;
@@ -32,7 +28,6 @@
 import android.os.LocaleList;
 import android.text.FontConfig;
 import android.util.ArraySet;
-import android.util.Log;
 import android.util.Xml;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -65,6 +60,7 @@
     private static final String VARIANT_ELEGANT = "elegant";
 
     // XML constants for Font.
+    public static final String ATTR_SUPPORTED_AXES = "supportedAxes";
     public static final String ATTR_INDEX = "index";
     public static final String ATTR_WEIGHT = "weight";
     public static final String ATTR_POSTSCRIPT_NAME = "postScriptName";
@@ -78,6 +74,10 @@
     public static final String ATTR_TAG = "tag";
     public static final String ATTR_STYLEVALUE = "stylevalue";
 
+    // The tag string for variable font type resolution.
+    private static final String TAG_WGHT = "wght";
+    private static final String TAG_ITAL = "ital";
+
     /* Parse fallback list (no names) */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
@@ -263,7 +263,6 @@
         final String lang = parser.getAttributeValue("", "lang");
         final String variant = parser.getAttributeValue(null, "variant");
         final String ignore = parser.getAttributeValue(null, "ignore");
-        final String varFamilyTypeStr = parser.getAttributeValue(null, "varFamilyType");
         final List<FontConfig.Font> fonts = new ArrayList<>();
         while (keepReading(parser)) {
             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
@@ -286,45 +285,12 @@
                 intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
             }
         }
-        int varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
-        if (varFamilyTypeStr != null) {
-            varFamilyType = Integer.parseInt(varFamilyTypeStr);
-            if (varFamilyType <= -1 || varFamilyType > 3) {
-                Log.e(TAG, "Error: unexpected varFamilyType value: " + varFamilyTypeStr);
-                varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
-            }
-
-            // validation but don't read font content for performance reasons.
-            switch (varFamilyType) {
-                case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY:
-                    if (fonts.size() != 1) {
-                        Log.e(TAG, "Error: Single font support wght axis, but two or more fonts are"
-                                + " included in the font family.");
-                        varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
-                    }
-                    break;
-                case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL:
-                    if (fonts.size() != 1) {
-                        Log.e(TAG, "Error: Single font support both ital and wght axes, but two or"
-                                + " more fonts are included in the font family.");
-                        varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
-                    }
-                    break;
-                case VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT:
-                    if (fonts.size() != 2) {
-                        Log.e(TAG, "Error: two fonts that support wght axis, but one or three or"
-                                + " more fonts are included in the font family.");
-                        varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
-                    }
-            }
-        }
 
         boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1")));
         if (skip || fonts.isEmpty()) {
             return null;
         }
-        return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant,
-                varFamilyType);
+        return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant);
     }
 
     private static void throwIfAttributeExists(String attrName, XmlPullParser parser) {
@@ -407,6 +373,7 @@
         boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
         String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
         String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
+        final String supportedAxes = parser.getAttributeValue(null, ATTR_SUPPORTED_AXES);
         StringBuilder filename = new StringBuilder();
         while (keepReading(parser)) {
             if (parser.getEventType() == XmlPullParser.TEXT) {
@@ -422,6 +389,18 @@
         }
         String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
 
+        int varTypeAxes = 0;
+        if (supportedAxes != null) {
+            for (String tag : supportedAxes.split(",")) {
+                String strippedTag = tag.strip();
+                if (strippedTag.equals(TAG_WGHT)) {
+                    varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_WGHT;
+                } else if (strippedTag.equals(TAG_ITAL)) {
+                    varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_ITAL;
+                }
+            }
+        }
+
         if (postScriptName == null) {
             // If post script name was not provided, assume the file name is same to PostScript
             // name.
@@ -462,7 +441,8 @@
                 ),
                 index,
                 varSettings,
-                fallbackFor);
+                fallbackFor,
+                varTypeAxes);
     }
 
     private static String findUpdatedFontFile(String psName,
diff --git a/graphics/java/android/graphics/Matrix44.java b/graphics/java/android/graphics/Matrix44.java
new file mode 100644
index 0000000..7cc0eb7
--- /dev/null
+++ b/graphics/java/android/graphics/Matrix44.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.graphics.hwui.flags.Flags;
+
+import java.util.Arrays;
+
+/**
+ * The Matrix44 class holds a 4x4 matrix for transforming coordinates. It is similar to
+ * {@link Matrix}, and should be used when you want to manipulate the canvas in 3D. Values are kept
+ * in row-major order. The values and operations are treated as column vectors.
+ */
+@FlaggedApi(Flags.FLAG_MATRIX_44)
+public class Matrix44 {
+    final float[] mBackingArray;
+    /**
+     * The default Matrix44 constructor will instantiate an identity matrix.
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public Matrix44() {
+        mBackingArray = new float[]{1.0f, 0.0f, 0.0f, 0.0f,
+                                    0.0f, 1.0f, 0.0f, 0.0f,
+                                    0.0f, 0.0f, 1.0f, 0.0f,
+                                    0.0f, 0.0f, 0.0f, 1.0f};
+    }
+
+    /**
+     * Creates and returns a Matrix44 by taking the 3x3 Matrix and placing it on the 0 of the z-axis
+     * by setting row {@code 2} and column {@code 2} to the identity as seen in the following
+     * operation:
+     * <pre class="prettyprint">
+     * [ a b c ]      [ a b 0 c ]
+     * [ d e f ]  ->  [ d e 0 f ]
+     * [ g h i ]      [ 0 0 1 0 ]
+     *                [ g h 0 i ]
+     * </pre>
+     *
+     * @param mat A 3x3 Matrix to be converted (original Matrix will not be changed)
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public Matrix44(@NonNull Matrix mat) {
+        float[] m = new float[9];
+        mat.getValues(m);
+        mBackingArray = new float[]{m[0], m[1], 0.0f, m[2],
+                                    m[3], m[4], 0.0f, m[5],
+                                    0.0f, 0.0f, 1.0f, 0.0f,
+                                    m[6], m[7], 0.0f, m[8]};
+    }
+
+    /**
+     * Copies matrix values into the provided array in row-major order.
+     *
+     * @param dst The float array where values will be copied, must be of length 16
+     * @throws IllegalArgumentException if the destination float array is not of length 16
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void getValues(@NonNull float [] dst) {
+        if (dst.length == 16) {
+            System.arraycopy(mBackingArray, 0, dst, 0, mBackingArray.length);
+        } else {
+            throw new IllegalArgumentException("Dst array must be of length 16");
+        }
+    }
+
+    /**
+     * Replaces the Matrix's values with the values in the provided array.
+     *
+     * @param src A float array of length 16. Floats are treated in row-major order
+     * @throws IllegalArgumentException if the destination float array is not of length 16
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void setValues(@NonNull float[] src) {
+        if (src.length == 16) {
+            System.arraycopy(src, 0, mBackingArray, 0, mBackingArray.length);
+        } else {
+            throw new IllegalArgumentException("Src array must be of length 16");
+        }
+    }
+
+    /**
+     * Gets the value at the matrix's row and column.
+     *
+     * @param row An integer from 0 to 4 indicating the row of the value to get
+     * @param col An integer from 0 to 4 indicating the column of the value to get
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public float get(int row, int col) {
+        if (row >= 0 && row < 4 && col >= 0 && col < 4) {
+            return mBackingArray[row * 4 + col];
+        }
+        throw new IllegalArgumentException("invalid row and column values");
+    }
+
+    /**
+     * Sets the value at the matrix's row and column to the provided value.
+     *
+     * @param row An integer from 0 to 4 indicating the row of the value to change
+     * @param col An integer from 0 to 4 indicating the column of the value to change
+     * @param val The value the element at the specified index will be set to
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void set(int row, int col, float val) {
+        if (row >= 0 && row < 4 && col >= 0 && col < 4) {
+            mBackingArray[row * 4 + col] = val;
+        } else {
+            throw new IllegalArgumentException("invalid row and column values");
+        }
+    }
+
+    /**
+     * Sets the Matrix44 to the identity matrix.
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void reset() {
+        for (int i = 0; i < mBackingArray.length; i++) {
+            mBackingArray[i] = (i % 4 == i / 4) ? 1.0f : 0.0f;
+        }
+    }
+
+    /**
+     * Inverts the Matrix44, then return true if successful, false if unable to invert.
+     *
+     * @return {@code true} on success, {@code false} otherwise
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public boolean invert() {
+        float a00 = mBackingArray[0];
+        float a01 = mBackingArray[1];
+        float a02 = mBackingArray[2];
+        float a03 = mBackingArray[3];
+        float a10 = mBackingArray[4];
+        float a11 = mBackingArray[5];
+        float a12 = mBackingArray[6];
+        float a13 = mBackingArray[7];
+        float a20 = mBackingArray[8];
+        float a21 = mBackingArray[9];
+        float a22 = mBackingArray[10];
+        float a23 = mBackingArray[11];
+        float a30 = mBackingArray[12];
+        float a31 = mBackingArray[13];
+        float a32 = mBackingArray[14];
+        float a33 = mBackingArray[15];
+        float b00 = a00 * a11 - a01 * a10;
+        float b01 = a00 * a12 - a02 * a10;
+        float b02 = a00 * a13 - a03 * a10;
+        float b03 = a01 * a12 - a02 * a11;
+        float b04 = a01 * a13 - a03 * a11;
+        float b05 = a02 * a13 - a03 * a12;
+        float b06 = a20 * a31 - a21 * a30;
+        float b07 = a20 * a32 - a22 * a30;
+        float b08 = a20 * a33 - a23 * a30;
+        float b09 = a21 * a32 - a22 * a31;
+        float b10 = a21 * a33 - a23 * a31;
+        float b11 = a22 * a33 - a23 * a32;
+        float det = (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06);
+        if (det == 0.0f) {
+            return false;
+        }
+        float invDet = 1.0f / det;
+        mBackingArray[0] = ((a11 * b11 - a12 * b10 + a13 * b09) * invDet);
+        mBackingArray[1] = ((-a01 * b11 + a02 * b10 - a03 * b09) * invDet);
+        mBackingArray[2] = ((a31 * b05 - a32 * b04 + a33 * b03) * invDet);
+        mBackingArray[3] = ((-a21 * b05 + a22 * b04 - a23 * b03) * invDet);
+        mBackingArray[4] = ((-a10 * b11 + a12 * b08 - a13 * b07) * invDet);
+        mBackingArray[5] = ((a00 * b11 - a02 * b08 + a03 * b07) * invDet);
+        mBackingArray[6] = ((-a30 * b05 + a32 * b02 - a33 * b01) * invDet);
+        mBackingArray[7] = ((a20 * b05 - a22 * b02 + a23 * b01) * invDet);
+        mBackingArray[8] = ((a10 * b10 - a11 * b08 + a13 * b06) * invDet);
+        mBackingArray[9] = ((-a00 * b10 + a01 * b08 - a03 * b06) * invDet);
+        mBackingArray[10] = ((a30 * b04 - a31 * b02 + a33 * b00) * invDet);
+        mBackingArray[11] = ((-a20 * b04 + a21 * b02 - a23 * b00) * invDet);
+        mBackingArray[12] = ((-a10 * b09 + a11 * b07 - a12 * b06) * invDet);
+        mBackingArray[13] = ((a00 * b09 - a01 * b07 + a02 * b06) * invDet);
+        mBackingArray[14] = ((-a30 * b03 + a31 * b01 - a32 * b00) * invDet);
+        mBackingArray[15] = ((a20 * b03 - a21 * b01 + a22 * b00) * invDet);
+        return true;
+    }
+
+    /**
+     * Returns true if Matrix44 is equal to identity matrix.
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public boolean isIdentity() {
+        for (int i = 0; i < mBackingArray.length; i++) {
+            float expected = (i % 4 == i / 4) ? 1.0f : 0.0f;
+            if (expected != mBackingArray[i]) return false;
+        }
+        return true;
+    }
+
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    private static float dot(Matrix44 a, Matrix44 b, int row, int col) {
+        return (a.get(row, 0) * b.get(0, col))
+                + (a.get(row, 1) * b.get(1, col))
+                + (a.get(row, 2) * b.get(2, col))
+                + (a.get(row, 3) * b.get(3, col));
+    }
+
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    private static float dot(float r0, float r1, float r2, float r3,
+                             float c0, float c1, float c2, float c3) {
+        return (r0 * c0) + (r1 * c1) + (r2 * c2) + (r3 * c3);
+    }
+
+    /**
+     * Multiplies (x, y, z, w) vector by the Matrix44, then returns the new (x, y, z, w). Users
+     * should set {@code w} to 1 to indicate the coordinates are normalized.
+     *
+     * @return An array of length 4 that represents the x, y, z, w (where w is perspective) value
+     * after multiplying x, y, z, 1 by the matrix
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public @NonNull float[] map(float x, float y, float z, float w) {
+        float[] dst = new float[4];
+        this.map(x, y, z, w, dst);
+        return dst;
+    }
+
+    /**
+     * Multiplies (x, y, z, w) vector by the Matrix44, then returns the new (x, y, z, w). Users
+     * should set {@code w} to 1 to indicate the coordinates are normalized.
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public void map(float x, float y, float z, float w, @NonNull float[] dst) {
+        if (dst.length != 4) {
+            throw new IllegalArgumentException("Dst array must be of length 4");
+        }
+        dst[0] = x * mBackingArray[0] + y * mBackingArray[1]
+                + z * mBackingArray[2] + w * mBackingArray[3];
+        dst[1] = x * mBackingArray[4] + y * mBackingArray[5]
+                + z * mBackingArray[6] + w * mBackingArray[7];
+        dst[2] = x * mBackingArray[8] + y * mBackingArray[9]
+                + z * mBackingArray[10] + w * mBackingArray[11];
+        dst[3] = x * mBackingArray[12] + y * mBackingArray[13]
+                + z * mBackingArray[14] + w * mBackingArray[15];
+    }
+
+    /**
+     * Multiplies `this` matrix (A) and provided Matrix (B) in the order of A * B.
+     * The result is saved in `this` Matrix.
+     *
+     * @param b The second Matrix in the concatenation operation
+     * @return A reference to this Matrix, which can be used to chain Matrix operations
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public @NonNull Matrix44 concat(@NonNull Matrix44 b) {
+        float val00 = dot(this, b, 0, 0);
+        float val01 = dot(this, b, 0, 1);
+        float val02 = dot(this, b, 0, 2);
+        float val03 = dot(this, b, 0, 3);
+        float val10 = dot(this, b, 1, 0);
+        float val11 = dot(this, b, 1, 1);
+        float val12 = dot(this, b, 1, 2);
+        float val13 = dot(this, b, 1, 3);
+        float val20 = dot(this, b, 2, 0);
+        float val21 = dot(this, b, 2, 1);
+        float val22 = dot(this, b, 2, 2);
+        float val23 = dot(this, b, 2, 3);
+        float val30 = dot(this, b, 3, 0);
+        float val31 = dot(this, b, 3, 1);
+        float val32 = dot(this, b, 3, 2);
+        float val33 = dot(this, b, 3, 3);
+
+        mBackingArray[0] = val00;
+        mBackingArray[1] = val01;
+        mBackingArray[2] = val02;
+        mBackingArray[3] = val03;
+        mBackingArray[4] = val10;
+        mBackingArray[5] = val11;
+        mBackingArray[6] = val12;
+        mBackingArray[7] = val13;
+        mBackingArray[8] = val20;
+        mBackingArray[9] = val21;
+        mBackingArray[10] = val22;
+        mBackingArray[11] = val23;
+        mBackingArray[12] = val30;
+        mBackingArray[13] = val31;
+        mBackingArray[14] = val32;
+        mBackingArray[15] = val33;
+
+        return this;
+    }
+
+    /**
+     * Applies a rotation around a given axis, then returns self.
+     * {@code x}, {@code y}, {@code z} represent the axis by which to rotate around.
+     * For example, pass in {@code 1, 0, 0} to rotate around the x-axis.
+     * The axis provided will be normalized.
+     *
+     * @param deg Amount in degrees to rotate the matrix about the x-axis
+     * @param xComp X component of the rotation axis
+     * @param yComp Y component of the rotation axis
+     * @param zComp Z component of the rotation axis
+     * @return A reference to this Matrix, which can be used to chain Matrix operations
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public @NonNull Matrix44 rotate(float deg, float xComp, float yComp, float zComp) {
+        float sum = xComp + yComp + zComp;
+        float x = xComp / sum;
+        float y = yComp / sum;
+        float z = zComp / sum;
+
+        float c = (float) Math.cos(deg * Math.PI / 180.0f);
+        float s = (float) Math.sin(deg * Math.PI / 180.0f);
+        float t = 1 - c;
+
+        float rotVals00 = t * x * x + c;
+        float rotVals01 = t * x * y - s * z;
+        float rotVals02 = t * x * z + s * y;
+        float rotVals10 = t * x * y + s * z;
+        float rotVals11 = t * y * y + c;
+        float rotVals12 = t * y * z - s * x;
+        float rotVals20 = t * x * z - s * y;
+        float rotVals21 = t * y * z + s * x;
+        float rotVals22 = t * z * z + c;
+
+        float v00 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3],
+                rotVals00, rotVals10, rotVals20, 0);
+        float v01 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3],
+                rotVals01, rotVals11, rotVals21, 0);
+        float v02 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3],
+                rotVals02, rotVals12, rotVals22, 0);
+        float v03 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3],
+                0, 0, 0, 1);
+        float v10 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7],
+                rotVals00, rotVals10, rotVals20, 0);
+        float v11 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7],
+                rotVals01, rotVals11, rotVals21, 0);
+        float v12 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7],
+                rotVals02, rotVals12, rotVals22, 0);
+        float v13 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7],
+                0, 0, 0, 1);
+        float v20 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11],
+                rotVals00, rotVals10, rotVals20, 0);
+        float v21 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11],
+                rotVals01, rotVals11, rotVals21, 0);
+        float v22 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11],
+                rotVals02, rotVals12, rotVals22, 0);
+        float v23 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11],
+                0, 0, 0, 1);
+        float v30 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15],
+                rotVals00, rotVals10, rotVals20, 0);
+        float v31 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15],
+                rotVals01, rotVals11, rotVals21, 0);
+        float v32 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15],
+                rotVals02, rotVals12, rotVals22, 0);
+        float v33 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15],
+                0, 0, 0, 1);
+
+        mBackingArray[0] = v00;
+        mBackingArray[1] = v01;
+        mBackingArray[2] = v02;
+        mBackingArray[3] = v03;
+        mBackingArray[4] = v10;
+        mBackingArray[5] = v11;
+        mBackingArray[6] = v12;
+        mBackingArray[7] = v13;
+        mBackingArray[8] = v20;
+        mBackingArray[9] = v21;
+        mBackingArray[10] = v22;
+        mBackingArray[11] = v23;
+        mBackingArray[12] = v30;
+        mBackingArray[13] = v31;
+        mBackingArray[14] = v32;
+        mBackingArray[15] = v33;
+
+        return this;
+    }
+
+    /**
+     * Applies scaling factors to `this` Matrix44, then returns self. Pass 1s for no change.
+     *
+     * @param x Scaling factor for the x-axis
+     * @param y Scaling factor for the y-axis
+     * @param z Scaling factor for the z-axis
+     * @return A reference to this Matrix, which can be used to chain Matrix operations
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public @NonNull Matrix44 scale(float x, float y, float z) {
+        mBackingArray[0] *= x;
+        mBackingArray[4] *= x;
+        mBackingArray[8] *= x;
+        mBackingArray[12] *= x;
+        mBackingArray[1] *= y;
+        mBackingArray[5] *= y;
+        mBackingArray[9] *= y;
+        mBackingArray[13] *= y;
+        mBackingArray[2] *= z;
+        mBackingArray[6] *= z;
+        mBackingArray[10] *= z;
+        mBackingArray[14] *= z;
+
+        return this;
+    }
+
+    /**
+     * Applies a translation to `this` Matrix44, then returns self.
+     *
+     * @param x Translation for the x-axis
+     * @param y Translation for the y-axis
+     * @param z Translation for the z-axis
+     * @return A reference to this Matrix, which can be used to chain Matrix operations
+     */
+    @FlaggedApi(Flags.FLAG_MATRIX_44)
+    public @NonNull Matrix44 translate(float x, float y, float z) {
+        float newX = x * mBackingArray[0] + y * mBackingArray[1]
+                + z * mBackingArray[2] + mBackingArray[3];
+        float newY = x * mBackingArray[4] + y * mBackingArray[5]
+                + z * mBackingArray[6] + mBackingArray[7];
+        float newZ = x * mBackingArray[8] + y * mBackingArray[9]
+                + z * mBackingArray[10] + mBackingArray[11];
+        float newW = x * mBackingArray[12] + y * mBackingArray[13]
+                + z * mBackingArray[14] + mBackingArray[15];
+
+        mBackingArray[3] = newX;
+        mBackingArray[7] = newY;
+        mBackingArray[11] = newZ;
+        mBackingArray[15] = newW;
+
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("""
+                        | %f %f %f %f |
+                        | %f %f %f %f |
+                        | %f %f %f %f |
+                        | %f %f %f %f |
+                        """, mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3],
+                mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7],
+                mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11],
+                mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15]);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof Matrix44) {
+            return Arrays.equals(mBackingArray, ((Matrix44) obj).mBackingArray);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) mBackingArray[0] + (int) mBackingArray[1] + (int) mBackingArray[2]
+                + (int) mBackingArray[3] + (int) mBackingArray[4] + (int) mBackingArray[5]
+                + (int) mBackingArray[6] + (int) mBackingArray[7] + (int) mBackingArray[8]
+                + (int) mBackingArray[9] + (int) mBackingArray[10] + (int) mBackingArray[11]
+                + (int) mBackingArray[12] + (int) mBackingArray[13] + (int) mBackingArray[14]
+                + (int) mBackingArray[15];
+    }
+
+}
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index 3ef714ed..a90961e 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -100,6 +100,71 @@
         }
     }
 
+    /** @hide */
+    @VisibleForTesting
+    public static @FontFamily.Builder.VariableFontFamilyType int resolveVarFamilyType(
+            @NonNull FontConfig.FontFamily xmlFamily,
+            @Nullable String familyName) {
+        int wghtCount = 0;
+        int italCount = 0;
+        int targetFonts = 0;
+        boolean hasItalicFont = false;
+
+        List<FontConfig.Font> fonts = xmlFamily.getFontList();
+        for (int i = 0; i < fonts.size(); ++i) {
+            FontConfig.Font font = fonts.get(i);
+
+            if (familyName == null) {  // for default family
+                if (font.getFontFamilyName() != null) {
+                    continue;  // this font is not for the default family.
+                }
+            } else {  // for the specific family
+                if (!familyName.equals(font.getFontFamilyName())) {
+                    continue;  // this font is not for given family.
+                }
+            }
+
+            final int varTypeAxes = font.getVarTypeAxes();
+            if (varTypeAxes == 0) {
+                // If we see static font, we can immediately return as VAR_TYPE_NONE.
+                return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+            }
+
+            if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_WGHT) != 0) {
+                wghtCount++;
+            }
+
+            if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_ITAL) != 0) {
+                italCount++;
+            }
+
+            if (font.getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC) {
+                hasItalicFont = true;
+            }
+            targetFonts++;
+        }
+
+        if (italCount == 0) {  // No ital font.
+            if (targetFonts == 1 && wghtCount == 1) {
+                // If there is only single font that has wght, use it for regular style and
+                // use synthetic bolding for italic.
+                return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+            } else if (targetFonts == 2 && wghtCount == 2 && hasItalicFont) {
+                // If there are two fonts and italic font is available, use them for regular and
+                // italic separately. (It is impossible to have two italic fonts. It will end up
+                // with Typeface creation failure.)
+                return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
+            }
+        } else if (italCount == 1) {
+            // If ital font is included, a single font should support both wght and ital.
+            if (wghtCount == 1 && targetFonts == 1) {
+                return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+            }
+        }
+        // Otherwise, unsupported.
+        return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+    }
+
     private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily,
             @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap,
             @NonNull Map<String, ByteBuffer> cache) {
@@ -126,7 +191,7 @@
         }
 
         final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
-                defaultFonts, languageTags, variant, xmlFamily.getVariableFontFamilyType(), false,
+                defaultFonts, languageTags, variant, resolveVarFamilyType(xmlFamily, null), false,
                 cache);
         // Insert family into fallback map.
         for (int i = 0; i < fallbackMap.size(); i++) {
@@ -145,7 +210,7 @@
                 }
             } else {
                 final FontFamily family = createFontFamily(fallback, languageTags, variant,
-                        xmlFamily.getVariableFontFamilyType(), false, cache);
+                        resolveVarFamilyType(xmlFamily, name), false, cache);
                 if (family != null) {
                     familyListSet.familyList.add(family);
                 } else if (defaultFamily != null) {
@@ -217,7 +282,8 @@
             final FontFamily family = createFontFamily(
                     xmlFamily.getFontList(),
                     xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(),
-                    xmlFamily.getVariableFontFamilyType(),
+                    resolveVarFamilyType(xmlFamily,
+                            null /* all fonts under named family should be treated as default */),
                     true, // named family is always default
                     bufferCache);
             if (family == null) {
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
index b21bf11..7d55928 100644
--- a/graphics/java/android/graphics/text/LineBreakConfig.java
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -176,6 +176,9 @@
      * - If at least one locale in the locale list contains Japanese script, this option is
      * equivalent to {@link #LINE_BREAK_STYLE_STRICT}.
      * - Otherwise, this option is equivalent to {@link #LINE_BREAK_STYLE_NONE}.
+     *
+     * <p>
+     * Note: future versions may have special line breaking style rules for other locales.
      */
     @FlaggedApi(FLAG_WORD_STYLE_AUTO)
     public static final int LINE_BREAK_STYLE_AUTO = 5;
@@ -249,6 +252,9 @@
      * option is equivalent to {@link #LINE_BREAK_WORD_STYLE_PHRASE} if the result of its line
      * count is less than 5 lines.
      * - Otherwise, this option is equivalent to {@link #LINE_BREAK_WORD_STYLE_NONE}.
+     *
+     * <p>
+     * Note: future versions may have special line breaking word style rules for other locales.
      */
     @FlaggedApi(FLAG_WORD_STYLE_AUTO)
     public static final int LINE_BREAK_WORD_STYLE_AUTO = 2;
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 4cdc06a..a12fa5f 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -39,6 +39,7 @@
 }
 
 // Sources that have no dependencies that can be used directly downstream of this library
+// TODO(b/322791067): move these sources to WindowManager-Shell-shared
 filegroup {
     name: "wm_shell_util-sources",
     srcs: [
@@ -137,6 +138,12 @@
     },
 }
 
+java_library {
+    name: "WindowManager-Shell-shared",
+
+    srcs: ["shared/**/*.java"],
+}
+
 android_library {
     name: "WindowManager-Shell",
     srcs: [
@@ -162,6 +169,7 @@
         "com_android_wm_shell_flags_lib",
         "com.android.window.flags.window-aconfig-java",
         "WindowManager-Shell-proto",
+        "WindowManager-Shell-shared",
         "perfetto_trace_java_protos",
         "dagger2",
         "jsr330",
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 0e04658..2b95f30 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -30,13 +30,6 @@
 }
 
 flag {
-    name: "enable_pip_ui_state_on_entering"
-    namespace: "multitasking"
-    description: "Enables PiP UI state callback on entering"
-    bug: "303718131"
-}
-
-flag {
     name: "enable_pip2_implementation"
     namespace: "multitasking"
     description: "Enables the new implementation of PiP (PiP2)"
@@ -57,3 +50,10 @@
     description: "Enables new animations for expand and collapse for bubbles"
     bug: "311450609"
 }
+
+flag {
+    name: "enable_pip_umo_experience"
+    namespace: "multitasking"
+    description: "Enables new UMO experience for PiP menu"
+    bug: "307998712"
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
index 4c76168..398fd55 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.taskview.TaskView
-import com.android.wm.shell.taskview.TaskViewTaskController
 
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -30,6 +29,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -37,10 +38,11 @@
 
     private lateinit var bubbleTaskView: BubbleTaskView
     private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var taskView: TaskView
 
     @Before
     fun setUp() {
-        val taskView = TaskView(context, mock<TaskViewTaskController>())
+        taskView = mock()
         bubbleTaskView = BubbleTaskView(taskView, directExecutor())
     }
 
@@ -72,4 +74,19 @@
         assertThat(actualTaskId).isEqualTo(123)
         assertThat(actualComponentName).isEqualTo(componentName)
     }
+
+    @Test
+    fun cleanup_invalidTaskId_doesNotRemoveTask() {
+        bubbleTaskView.cleanup()
+        verify(taskView, never()).removeTask()
+    }
+
+    @Test
+    fun cleanup_validTaskId_removesTask() {
+        val componentName = ComponentName(context, "TestClass")
+        bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+        bubbleTaskView.cleanup()
+        verify(taskView).removeTask()
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
index 7e95814..fd3a749 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.util;
+package com.android.wm.shell.shared;
 
 import android.graphics.Point;
 import android.util.RotationUtils;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 936faa3..dcd4062 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.util;
+package com.android.wm.shell.shared;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.RemoteAnimationTarget.MODE_CHANGING;
@@ -28,14 +28,13 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
-import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -53,6 +52,8 @@
 
 /** Various utility functions for transitions. */
 public class TransitionUtil {
+    /** Flag applied to a transition change to identify it as a divider bar for animation. */
+    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
 
     /** @return true if the transition was triggered by opening something vs closing something */
     public static boolean isOpeningType(@WindowManager.TransitionType int type) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 8241e1a..8d30db6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -29,7 +29,7 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 /**
  * Wrapper to handle the ActivityEmbedding animation update in one
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 44ee561..539832e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -46,7 +46,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter;
 import com.android.wm.shell.common.ScreenshotUtils;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index efa5a1a..0272f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -38,7 +38,7 @@
 import android.window.TransitionInfo;
 
 import com.android.internal.policy.TransitionAnimation;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 /** Animation spec for ActivityEmbedding transition. */
 // TODO(b/206557124): provide an easier way to customize animation
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index b4e852c..1f9358e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -38,9 +38,9 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.List;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 87c8f52..f32f030 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -51,6 +51,8 @@
 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
 import com.android.wm.shell.common.bubbles.BubbleInfo;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -105,6 +107,8 @@
     private BubbleExpandedView mExpandedView;
     @Nullable
     private BubbleBarExpandedView mBubbleBarExpandedView;
+    @Nullable
+    private BubbleTaskView mBubbleTaskView;
 
     private BubbleViewInfoTask mInflationTask;
     private boolean mInflateSynchronously;
@@ -394,6 +398,21 @@
     }
 
     /**
+     * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new
+     * instance of {@link BubbleTaskView} is created.
+     */
+    public BubbleTaskView getOrCreateBubbleTaskView(Context context, BubbleController controller) {
+        if (mBubbleTaskView == null) {
+            TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
+                    controller.getTaskOrganizer(),
+                    controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
+            TaskView taskView = new TaskView(context, taskViewTaskController);
+            mBubbleTaskView = new BubbleTaskView(taskView, controller.getMainExecutor());
+        }
+        return mBubbleTaskView;
+    }
+
+    /**
      * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
      */
     String getShortcutId() {
@@ -415,6 +434,10 @@
      * the bubble.
      */
     void cleanupExpandedView() {
+        cleanupExpandedView(true);
+    }
+
+    private void cleanupExpandedView(boolean cleanupTaskView) {
         if (mExpandedView != null) {
             mExpandedView.cleanUpExpandedState();
             mExpandedView = null;
@@ -423,17 +446,37 @@
             mBubbleBarExpandedView.cleanUpExpandedState();
             mBubbleBarExpandedView = null;
         }
+        if (cleanupTaskView) {
+            cleanupTaskView();
+        }
         if (mIntent != null) {
             mIntent.unregisterCancelListener(mIntentCancelListener);
         }
         mIntentActive = false;
     }
 
+    private void cleanupTaskView() {
+        if (mBubbleTaskView != null) {
+            mBubbleTaskView.cleanup();
+            mBubbleTaskView = null;
+        }
+    }
+
     /**
      * Call when all the views should be removed/cleaned up.
      */
     void cleanupViews() {
-        cleanupExpandedView();
+        cleanupViews(true);
+    }
+
+    /**
+     * Call when all the views should be removed/cleaned up.
+     *
+     * <p>If we're switching between bar and floating modes, pass {@code false} on
+     * {@code cleanupTaskView} to avoid recreating it in the new mode.
+     */
+    void cleanupViews(boolean cleanupTaskView) {
+        cleanupExpandedView(cleanupTaskView);
         mIconView = null;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index a5f7880..aea3ca1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -173,7 +173,7 @@
     private final Context mContext;
     private final BubblesImpl mImpl = new BubblesImpl();
     private Bubbles.BubbleExpandListener mExpandListener;
-    @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
+    @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
     private final FloatingContentCoordinator mFloatingContentCoordinator;
     private final BubbleDataRepository mDataRepository;
     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
@@ -197,12 +197,12 @@
     private final Handler mMainHandler;
     private final ShellExecutor mBackgroundExecutor;
 
-    private BubbleLogger mLogger;
-    private BubbleData mBubbleData;
+    private final BubbleLogger mLogger;
+    private final BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
     @Nullable private BubbleBarLayerView mLayerView;
     private BubbleIconFactory mBubbleIconFactory;
-    private BubblePositioner mBubblePositioner;
+    private final BubblePositioner mBubblePositioner;
     private Bubbles.SysuiProxy mSysuiProxy;
 
     // Tracks the id of the current (foreground) user.
@@ -232,13 +232,17 @@
     /** Whether or not the BubbleStackView has been added to the WindowManager. */
     private boolean mAddedToWindowManager = false;
 
-    /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */
+    /**
+     * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}.
+     */
     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
 
-    /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/
-    private Rect mScreenBounds = new Rect();
+    /**
+     * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}.
+     */
+    private final Rect mScreenBounds = new Rect();
 
-    /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */
+    /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */
     private float mFontScale = 0;
 
     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
@@ -253,9 +257,9 @@
     private boolean mIsStatusBarShade = true;
 
     /** One handed mode controller to register transition listener. */
-    private Optional<OneHandedController> mOneHandedOptional;
+    private final Optional<OneHandedController> mOneHandedOptional;
     /** Drag and drop controller to register listener for onDragStarted. */
-    private Optional<DragAndDropController> mDragAndDropController;
+    private final Optional<DragAndDropController> mDragAndDropController;
     /** Used to send bubble events to launcher. */
     private Bubbles.BubbleStateListener mBubbleStateListener;
 
@@ -731,9 +735,11 @@
             }
         } else {
             if (mStackView == null) {
+                BubbleStackViewManager bubbleStackViewManager =
+                        BubbleStackViewManager.fromBubbleController(this);
                 mStackView = new BubbleStackView(
-                        mContext, this, mBubbleData, mSurfaceSynchronizer,
-                        mFloatingContentCoordinator, this, mMainExecutor);
+                        mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData,
+                        mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor);
                 mStackView.onOrientationChanged();
                 if (mExpandListener != null) {
                     mStackView.setExpandListener(mExpandListener);
@@ -893,7 +899,6 @@
      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
      * added in the meantime.
      */
-    @VisibleForTesting
     public void onAllBubblesAnimatedOut() {
         if (mStackView != null) {
             mStackView.setVisibility(INVISIBLE);
@@ -1047,7 +1052,6 @@
         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
     }
 
-    @VisibleForTesting
     public boolean isStackExpanded() {
         return mBubbleData.isExpanded();
     }
@@ -1105,9 +1109,8 @@
      * <p>This is used by external callers (launcher).
      */
     @VisibleForTesting
-    public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX,
-            int bubbleBarOffsetY) {
-        mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY);
+    public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) {
+        mBubblePositioner.setBubbleBarPosition(bubbleBarBounds);
 
         if (BubbleOverflow.KEY.equals(key)) {
             mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1273,9 +1276,17 @@
                 mBubbleData.setExpanded(true);
             }
         } else {
-            // App bubble does not exist, lets add and expand it
-            Log.i(TAG, "  showOrHideAppBubble, creating and expanding app bubble");
-            Bubble b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
+            // Check if it exists in the overflow
+            Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
+            if (b != null) {
+                // It's in the overflow, so remove it & reinflate
+                Log.i(TAG, "  showOrHideAppBubble, expanding app bubble from overflow");
+                mBubbleData.removeOverflowBubble(b);
+            } else {
+                // App bubble does not exist, lets add and expand it
+                Log.i(TAG, "  showOrHideAppBubble, creating and expanding app bubble");
+                b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
+            }
             b.setShouldAutoExpand(true);
             inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
         }
@@ -1366,8 +1377,9 @@
             mStackView.resetOverflowView();
             mStackView.removeAllViews();
         }
-        // cleanup existing bubble views so they can be recreated later if needed.
-        mBubbleData.getBubbles().forEach(Bubble::cleanupViews);
+        // cleanup existing bubble views so they can be recreated later if needed, but retain
+        // TaskView.
+        mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false));
 
         // remove the current bubble container from window manager, null it out, and create a new
         // container based on the current mode.
@@ -1478,7 +1490,6 @@
      * <p>
      * Must be called from the main thread.
      */
-    @VisibleForTesting
     @MainThread
     public void removeBubble(String key, int reason) {
         if (mBubbleData.hasAnyBubbleWithKey(key)) {
@@ -1486,36 +1497,6 @@
         }
     }
 
-    // TODO(b/316358859): remove this method after task views are shared across modes
-    /**
-     * Removes the bubble with the given key after task removal, unless the task was removed as
-     * a result of mode switching, in which case, the bubble isn't removed because it will be
-     * re-inflated for the new mode.
-     */
-    @MainThread
-    public void removeFloatingBubbleAfterTaskRemoval(String key, int reason) {
-        // if we're floating remove the bubble. otherwise, we're here because the task was removed
-        // after switching modes. See b/316358859
-        if (!isShowingAsBubbleBar()) {
-            removeBubble(key, reason);
-        }
-    }
-
-    // TODO(b/316358859): remove this method after task views are shared across modes
-    /**
-     * Removes the bubble with the given key after task removal, unless the task was removed as
-     * a result of mode switching, in which case, the bubble isn't removed because it will be
-     * re-inflated for the new mode.
-     */
-    @MainThread
-    public void removeBarBubbleAfterTaskRemoval(String key, int reason) {
-        // if we're showing as bubble bar remove the bubble. otherwise, we're here because the task
-        // was removed after switching modes. See b/316358859
-        if (isShowingAsBubbleBar()) {
-            removeBubble(key, reason);
-        }
-    }
-
     /**
      * Removes all the bubbles.
      * <p>
@@ -2198,10 +2179,10 @@
         }
 
         @Override
-        public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
+        public void showBubble(String key, Rect bubbleBarBounds) {
             mMainExecutor.execute(
                     () -> mController.expandStackAndSelectBubbleFromLauncher(
-                            key, bubbleBarOffsetX, bubbleBarOffsetY));
+                            key, bubbleBarBounds));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 6e0c804..127c7e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -491,6 +491,19 @@
     }
 
     /**
+     * Explicitly removes a bubble from the overflow, if it exists.
+     *
+     * @param bubble the bubble to remove.
+     */
+    public void removeOverflowBubble(Bubble bubble) {
+        if (bubble == null) return;
+        if (mOverflowBubbles.remove(bubble)) {
+            mStateChange.removedOverflowBubble = bubble;
+            dispatchPendingChanges();
+        }
+    }
+
+    /**
      * Adds a group key indicating that the summary for this group should be suppressed.
      *
      * @param groupKey the group key of the group whose summary should be suppressed.
@@ -1145,7 +1158,6 @@
         return null;
     }
 
-    @VisibleForTesting(visibility = PRIVATE)
     public Bubble getOverflowBubbleWithKey(String key) {
         for (int i = 0; i < mOverflowBubbles.size(); i++) {
             Bubble bubble = mOverflowBubbles.get(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 9f7d0ac..efc4d8b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -27,12 +27,10 @@
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
-import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -49,7 +47,6 @@
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
-import android.os.RemoteException;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.IntProperty;
@@ -311,8 +308,7 @@
                         + " bubble=" + getBubbleKey());
             }
             if (mBubble != null) {
-                mController.removeFloatingBubbleAfterTaskRemoval(
-                        mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+                mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
             }
             if (mTaskView != null) {
                 // Release the surface
@@ -1105,32 +1101,11 @@
         return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
     }
 
-    /**
-     * Cleans up anything related to the task. The TaskView itself is released after the task
-     * has been removed.
-     *
-     * If this view should be reused after this method is called, then
-     * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)}
-     * must be invoked first.
-     */
+    /** Hide the task view. */
     public void cleanUpExpandedState() {
         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
             Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
         }
-        if (getTaskId() != INVALID_TASK_ID) {
-            // Ensure the task is removed from WM
-            if (ENABLE_SHELL_TRANSITIONS) {
-                if (mTaskView != null) {
-                    mTaskView.removeTask();
-                }
-            } else {
-                try {
-                    ActivityTaskManager.getService().removeTask(getTaskId());
-                } catch (RemoteException e) {
-                    Log.w(TAG, e.getMessage());
-                }
-            }
-        }
         if (mTaskView != null) {
             mTaskView.setVisibility(GONE);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index a76bd26..d62c86c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -97,7 +96,7 @@
     private PointF mRestingStackPosition;
 
     private boolean mShowingInBubbleBar;
-    private final Point mBubbleBarPosition = new Point();
+    private final Rect mBubbleBarBounds = new Rect();
 
     public BubblePositioner(Context context, WindowManager windowManager) {
         mContext = context;
@@ -791,15 +790,10 @@
     }
 
     /**
-     * Sets the position of the bubble bar in screen coordinates.
-     *
-     * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis
-     * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis
+     * Sets the position of the bubble bar in display coordinates.
      */
-    public void setBubbleBarPosition(int offsetX, int offsetY) {
-        mBubbleBarPosition.set(
-                getAvailableRect().width() - offsetX,
-                getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY);
+    public void setBubbleBarPosition(Rect bubbleBarBounds) {
+        mBubbleBarBounds.set(bubbleBarBounds);
     }
 
     /**
@@ -820,7 +814,7 @@
 
     /** The bottom position of the expanded view when showing above the bubble bar. */
     public int getExpandedViewBottomForBubbleBar() {
-        return mBubbleBarPosition.y - mExpandedViewPadding;
+        return mBubbleBarBounds.top - mExpandedViewPadding;
     }
 
     /**
@@ -831,9 +825,9 @@
     }
 
     /**
-     * Returns the on screen co-ordinates of the bubble bar.
+     * Returns the display coordinates of the bubble bar.
      */
-    public Point getBubbleBarPosition() {
-        return mBubbleBarPosition;
+    public Rect getBubbleBarBounds() {
+        return mBubbleBarBounds;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index a619401..9facef3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -204,7 +204,7 @@
                     Choreographer.getInstance().postFrameCallback(frameCallback);
                 }
             };
-    private final BubbleController mBubbleController;
+    private final BubbleStackViewManager mManager;
     private final BubbleData mBubbleData;
     private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider;
     private StackViewState mStackViewState = new StackViewState();
@@ -858,6 +858,7 @@
 
     private BubbleOverflow mBubbleOverflow;
     private StackEducationView mStackEduView;
+    private StackEducationView.Manager mStackEducationViewManager;
     private ManageEducationView mManageEduView;
     private DismissView mDismissView;
 
@@ -873,15 +874,16 @@
     private BubblePositioner mPositioner;
 
     @SuppressLint("ClickableViewAccessibility")
-    public BubbleStackView(Context context, BubbleController bubbleController,
-            BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
+    public BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager,
+            BubblePositioner bubblePositioner, BubbleData data,
+            @Nullable SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator,
             Bubbles.SysuiProxy.Provider sysuiProxyProvider,
             ShellExecutor mainExecutor) {
         super(context);
 
         mMainExecutor = mainExecutor;
-        mBubbleController = bubbleController;
+        mManager = bubbleStackViewManager;
         mBubbleData = data;
         mSysuiProxyProvider = sysuiProxyProvider;
 
@@ -893,7 +895,7 @@
         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
         int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
 
-        mPositioner = mBubbleController.getPositioner();
+        mPositioner = bubblePositioner;
 
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[]{android.R.attr.dialogCornerRadius});
@@ -903,7 +905,7 @@
         final Runnable onBubbleAnimatedOut = () -> {
             if (getBubbleCount() == 0) {
                 mExpandedViewTemporarilyHidden = false;
-                mBubbleController.onAllBubblesAnimatedOut();
+                mManager.onAllBubblesAnimatedOut();
             }
         };
         mStackAnimationController = new StackAnimationController(
@@ -1383,7 +1385,9 @@
             return false;
         }
         if (mStackEduView == null) {
-            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+            mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+            mStackEduView =
+                    new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
             addView(mStackEduView);
         }
         return showStackEdu();
@@ -1412,7 +1416,9 @@
     private void updateUserEdu() {
         if (isStackEduVisible() && !mStackEduView.isHiding()) {
             removeView(mStackEduView);
-            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+            mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+            mStackEduView =
+                    new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
             addView(mStackEduView);
             showStackEdu();
         }
@@ -2106,7 +2112,7 @@
             logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
             logBubbleEvent(mExpandedBubble,
                     FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
-            mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
+            mManager.checkNotificationPanelExpandedState(notifPanelExpanded -> {
                 if (!notifPanelExpanded && mIsExpanded) {
                     startMonitoringSwipeUpGesture();
                 }
@@ -2227,7 +2233,7 @@
      */
     void hideCurrentInputMethod() {
         mPositioner.setImeVisible(false, 0);
-        mBubbleController.hideCurrentInputMethod();
+        mManager.hideCurrentInputMethod();
     }
 
     /** Set the stack position to whatever the positioner says. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
new file mode 100644
index 0000000..fb597a0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.bubbles
+
+import java.util.function.Consumer
+
+/** Defines callbacks from [BubbleStackView] to its manager. */
+interface BubbleStackViewManager {
+
+    /** Notifies that all bubbles animated out. */
+    fun onAllBubblesAnimatedOut()
+
+    /** Notifies whether backpress should be intercepted. */
+    fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+
+    /**
+     * Checks the current expansion state of the notification panel, and invokes [callback] with the
+     * result.
+     */
+    fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>)
+
+    /** Requests to hide the current input method. */
+    fun hideCurrentInputMethod()
+
+    companion object {
+
+        @JvmStatic
+        fun fromBubbleController(controller: BubbleController) = object : BubbleStackViewManager {
+            override fun onAllBubblesAnimatedOut() {
+                controller.onAllBubblesAnimatedOut()
+            }
+
+            override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {
+                controller.updateWindowFlagsForBackpress(interceptBack)
+            }
+
+            override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {
+                controller.isNotificationPanelExpanded(callback)
+            }
+
+            override fun hideCurrentInputMethod() {
+                controller.hideCurrentInputMethod()
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
index 2fcd133..65f8e48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
@@ -16,10 +16,14 @@
 
 package com.android.wm.shell.bubbles
 
+import android.app.ActivityTaskManager
 import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.content.ComponentName
+import android.os.RemoteException
+import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import java.util.concurrent.Executor
 
 /**
@@ -78,4 +82,28 @@
     init {
         taskView.setListener(executor, listener)
     }
+
+    /**
+     * Removes the [TaskView] from window manager.
+     *
+     * This should be called after all other cleanup animations have finished.
+     */
+    fun cleanup() {
+        if (taskId != INVALID_TASK_ID) {
+            // Ensure the task is removed from WM
+            if (ENABLE_SHELL_TRANSITIONS) {
+                taskView.removeTask()
+            } else {
+                try {
+                    ActivityTaskManager.getService().removeTask(taskId)
+                } catch (e: RemoteException) {
+                    Log.w(TAG, e.message ?: "")
+                }
+            }
+        }
+    }
+
+    private companion object {
+        const val TAG = "BubbleTaskView"
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5855a81..5fc67d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.os.RemoteException;
 import android.util.Log;
 import android.view.View;
 
@@ -183,8 +182,11 @@
                         + " bubble=" + getBubbleKey());
             }
             if (mBubble != null) {
-                mController.removeBarBubbleAfterTaskRemoval(
-                        mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+                mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+            }
+            if (mTaskView != null) {
+                mTaskView.release();
+                mTaskView = null;
             }
         }
 
@@ -228,24 +230,6 @@
         return false;
     }
 
-    /** Cleans up anything related to the task and {@code TaskView}. */
-    public void cleanUpTaskView() {
-        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
-            Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
-        }
-        if (mTaskId != INVALID_TASK_ID) {
-            try {
-                ActivityTaskManager.getService().removeTask(mTaskId);
-            } catch (RemoteException e) {
-                Log.w(TAG, e.getMessage());
-            }
-        }
-        if (mTaskView != null) {
-            mTaskView.release();
-            mTaskView = null;
-        }
-    }
-
     /** Returns the bubble key associated with this view. */
     @Nullable
     public String getBubbleKey() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index c3d899e..5fc10a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -46,8 +46,6 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
-import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
 
 import java.lang.ref.WeakReference;
 import java.util.Objects;
@@ -175,7 +173,7 @@
             BubbleViewInfo info = new BubbleViewInfo();
 
             if (!skipInflation && !b.isInflated()) {
-                BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
                         R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
@@ -205,7 +203,7 @@
                         R.layout.bubble_view, stackView, false /* attachToRoot */);
                 info.imageView.initialize(controller.getPositioner());
 
-                BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
                 info.expandedView = (BubbleExpandedView) inflater.inflate(
                         R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
                 info.expandedView.initialize(
@@ -225,15 +223,6 @@
             }
             return info;
         }
-
-        private static BubbleTaskView createBubbleTaskView(
-                Context context, BubbleController controller) {
-            TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
-                    controller.getTaskOrganizer(),
-                    controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
-            TaskView taskView = new TaskView(context, taskViewTaskController);
-            return new BubbleTaskView(taskView, controller.getMainExecutor());
-        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
index 9e8a385..c1f704a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
@@ -24,8 +24,8 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 /**
  * Observer used to identify tasks that are opening or moving to front. If a bubble activity is
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 5776ad1..7a5afec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -17,6 +17,7 @@
 package com.android.wm.shell.bubbles;
 
 import android.content.Intent;
+import android.graphics.Rect;
 import com.android.wm.shell.bubbles.IBubblesListener;
 
 /**
@@ -29,7 +30,7 @@
 
     oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
 
-    oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3;
+    oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3;
 
     oneway void removeBubble(in String key) = 4;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 95f1017..c4108c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -35,7 +35,7 @@
 class StackEducationView(
     context: Context,
     private val positioner: BubblePositioner,
-    private val controller: BubbleController
+    private val manager: Manager
 ) : LinearLayout(context) {
 
     companion object {
@@ -44,6 +44,12 @@
         private const val ANIMATE_DURATION_SHORT: Long = 40
     }
 
+    /** Callbacks to notify managers of [StackEducationView] about events. */
+    interface Manager {
+        /** Notifies whether backpress should be intercepted. */
+        fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+    }
+
     private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
     private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
     private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) }
@@ -93,7 +99,7 @@
     override fun onDetachedFromWindow() {
         super.onDetachedFromWindow()
         setOnKeyListener(null)
-        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(false /* interceptBack */)
     }
 
     private fun setTextColor() {
@@ -124,7 +130,7 @@
         isHiding = false
         if (visibility == VISIBLE) return false
 
-        controller.updateWindowFlagsForBackpress(true /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(true /* interceptBack */)
         layoutParams.width =
                 if (positioner.isLargeScreen || positioner.isLandscape)
                     context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width)
@@ -185,7 +191,7 @@
         if (visibility != VISIBLE || isHiding) return
         isHiding = true
 
-        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(false /* interceptBack */)
         animate()
             .alpha(0f)
             .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 893a87f..84a616f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -22,6 +22,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.util.Log;
 import android.util.Size;
 import android.view.View;
@@ -149,12 +150,12 @@
         bbev.setVisibility(VISIBLE);
 
         // Set the pivot point for the scale, so the view animates out from the bubble bar.
-        Point bubbleBarPosition = mPositioner.getBubbleBarPosition();
+        Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
         mExpandedViewContainerMatrix.setScale(
                 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                bubbleBarPosition.x,
-                bubbleBarPosition.y);
+                bubbleBarBounds.centerX(),
+                bubbleBarBounds.top);
 
         bbev.setAnimationMatrix(mExpandedViewContainerMatrix);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 3cf23ac..00d683e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -272,7 +272,6 @@
             if (mTaskView != null) {
                 removeView(mTaskView);
             }
-            mBubbleTaskViewHelper.cleanUpTaskView();
         }
         mMenuViewController.hideMenu(false /* animated */);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
similarity index 88%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
index 0775f52..2f1189a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
 
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
@@ -33,12 +33,13 @@
 import android.view.WindowManager;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
 
 import java.util.List;
 
 /**
- *  Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into
- *  PiP menu when certain events happen (task appear/vanish, PiP move, etc.)
+ *  Interface to interact with PiP menu when certain events happen
+ *  (task appear/vanish, PiP move, etc.).
  */
 public interface PipMenuController {
 
@@ -52,15 +53,15 @@
     float ALPHA_NO_CHANGE = -1f;
 
     /**
-     * Called when
-     * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
+     * Called when out implementation of
+     * {@link ShellTaskOrganizer.TaskListener#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
      * is called.
      */
     void attach(SurfaceControl leash);
 
     /**
-     * Called when
-     * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called.
+     * Called when our implementation of
+     * {@link ShellTaskOrganizer.TaskListener#onTaskVanished(RunningTaskInfo)} is called.
      */
     void detach();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index 49db8d9..e8c809e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -20,10 +20,11 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
 
 import android.annotation.IntDef;
 
+import com.android.wm.shell.shared.TransitionUtil;
+
 /** Helper utility class of methods and constants that are available to be imported in Launcher. */
 public class SplitScreenConstants {
     /** Duration used for every split fade-in or fade-out. */
@@ -126,7 +127,7 @@
             WINDOWING_MODE_FREEFORM};
 
     /** Flag applied to a transition change to identify it as a divider bar for animation. */
-    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+    public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR;
 
     public static final String splitPositionToString(@SplitPosition int pos) {
         switch (pos) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 7b98fa6..8eecf1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -18,18 +18,23 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.Handler;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.pip2.phone.PhonePipMenuController;
 import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipScheduler;
 import com.android.wm.shell.pip2.phone.PipTransition;
@@ -86,4 +91,16 @@
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipScheduler(context, pipBoundsState, mainExecutor);
     }
+
+    @WMSingleton
+    @Provides
+    static PhonePipMenuController providePipPhoneMenuController(Context context,
+            PipBoundsState pipBoundsState, PipMediaController pipMediaController,
+            SystemWindows systemWindows,
+            PipUiEventLogger pipUiEventLogger,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler) {
+        return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
+                systemWindows, pipUiEventLogger, mainExecutor, mainHandler);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index c0fc02fa..f82212d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -86,10 +86,10 @@
     ) {
         visibleTasksListeners[visibleTasksListener] = executor
         displayData.keyIterator().forEach { displayId ->
-            val visibleTasks = getVisibleTaskCount(displayId)
+            val visibleTasksCount = getVisibleTaskCount(displayId)
             val stashed = isStashed(displayId)
             executor.execute {
-                visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0)
+                visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
                 visibleTasksListener.onStashedChanged(displayId, stashed)
             }
         }
@@ -222,10 +222,8 @@
             val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
             for (otherDisplayId in otherDisplays) {
                 if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
-                    // Task removed from other display, check if we should notify listeners
-                    if (displayData[otherDisplayId].visibleTasks.isEmpty()) {
-                        notifyVisibleTaskListeners(otherDisplayId, hasVisibleFreeformTasks = false)
-                    }
+                    notifyVisibleTaskListeners(otherDisplayId,
+                        displayData[otherDisplayId].visibleTasks.size)
                 }
             }
         }
@@ -248,15 +246,15 @@
             )
         }
 
-        // Check if count changed and if there was no tasks or this is the first task
-        if (prevCount != newCount && (prevCount == 0 || newCount == 0)) {
-            notifyVisibleTaskListeners(displayId, newCount > 0)
+        // Check if count changed
+        if (prevCount != newCount) {
+            notifyVisibleTaskListeners(displayId, newCount)
         }
     }
 
-    private fun notifyVisibleTaskListeners(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+    private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
         visibleTasksListeners.forEach { (listener, executor) ->
-            executor.execute { listener.onVisibilityChanged(displayId, hasVisibleFreeformTasks) }
+            executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
         }
     }
 
@@ -379,9 +377,9 @@
      */
     interface VisibleTasksListener {
         /**
-         * Called when the desktop starts or stops showing freeform tasks.
+         * Called when the desktop changes the number of visible freeform tasks.
          */
-        fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
+        fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
 
         /**
          * Called when the desktop stashed status changes.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 28c06a4..a089e81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -106,8 +106,8 @@
         visualIndicator = null
     }
     private val taskVisibilityListener = object : VisibleTasksListener {
-        override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
-            launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
+        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+            launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
         }
     }
     private val dragToDesktopStateListener = object : DragToDesktopStateListener {
@@ -1033,14 +1033,16 @@
                 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
 
         private val listener: VisibleTasksListener = object : VisibleTasksListener {
-            override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
+            override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
                 KtProtoLog.v(
                         WM_SHELL_DESKTOP_MODE,
-                        "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b",
+                        "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
                         displayId,
-                        visible
+                        visibleTasksCount
                 )
-                remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
+                remoteListener.call {
+                    l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+                }
             }
 
             override fun onStashedChanged(displayId: Int, stashed: Boolean) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index c3a82ce..39610e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -26,6 +26,7 @@
 import android.window.WindowContainerTransaction
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
@@ -33,7 +34,6 @@
 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
 import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.util.KtProtoLog
-import com.android.wm.shell.util.TransitionUtil
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
@@ -239,7 +239,7 @@
                     show(change.leash)
                 }
             } else if (TransitionInfo.isIndependent(change, info)) {
-                // Root.
+                // Root(s).
                 when (state) {
                     is TransitionState.FromSplit -> {
                         state.splitRootChange = change
@@ -256,6 +256,9 @@
                         }
                     }
                     is TransitionState.FromFullscreen -> {
+                        // Most of the time we expect one change/task here, which should be the
+                        // same that initiated the drag and that should be layered on top of
+                        // everything.
                         if (change.taskInfo?.taskId == state.draggedTaskId) {
                             state.draggedTaskChange = change
                             val bounds = change.endAbsBounds
@@ -265,7 +268,18 @@
                                 show(change.leash)
                             }
                         } else {
-                            throw IllegalStateException("Expected root to be dragged task")
+                            // It's possible to see an additional change that isn't the dragged
+                            // task when the dragged task is translucent and so the task behind it
+                            // is included in the transition since it was visible and is now being
+                            // occluded by the Home task. Just layer it at the bottom and save it
+                            // in case we need to restore order if the drag is cancelled.
+                            state.otherRootChanges.add(change)
+                            val bounds = change.endAbsBounds
+                            startTransaction.apply {
+                                setLayer(change.leash, appLayers - i)
+                                setWindowCrop(change.leash, bounds.width(), bounds.height())
+                                show(change.leash)
+                            }
                         }
                     }
                 }
@@ -515,8 +529,18 @@
         val wct = WindowContainerTransaction()
         when (state) {
             is TransitionState.FromFullscreen -> {
+                // There may have been tasks sent behind home that are not the dragged task (like
+                // when the dragged task is translucent and that makes the task behind it visible).
+                // Restore the order of those first.
+                state.otherRootChanges.mapNotNull { it.container }.forEach { wc ->
+                    // TODO(b/322852244): investigate why even though these "other" tasks are
+                    //  reordered in front of home and behind the translucent dragged task, its
+                    //  surface is not visible on screen.
+                    wct.reorder(wc, true /* toTop */)
+                }
                 val wc = state.draggedTaskChange?.container
                         ?: error("Dragged task should be non-null before cancelling")
+                // Then the dragged task a the very top.
                 wct.reorder(wc, true /* toTop */)
             }
             is TransitionState.FromSplit -> {
@@ -574,6 +598,7 @@
                 override var draggedTaskChange: Change? = null,
                 override var cancelled: Boolean = false,
                 override var startAborted: Boolean = false,
+                var otherRootChanges: MutableList<Change> = mutableListOf()
         ) : TransitionState()
         data class FromSplit(
                 override val draggedTaskId: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
index 39128a8..8ed87f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -22,8 +22,8 @@
  */
 interface IDesktopTaskListener {
 
-    /** Desktop task visibility has change. Visible if at least 1 task is visible. */
-    oneway void onVisibilityChanged(int displayId, boolean visible);
+    /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */
+    oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount);
 
     /** Desktop task stashed status has changed. */
     oneway void onStashedChanged(int displayId, boolean stashed);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index e63bbc0..73de231 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -27,7 +27,7 @@
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.view.WindowManager.TRANSIT_SLEEP;
 
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
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 07b8f11..52a06e0 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
@@ -86,6 +86,7 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 8e375a9..896ca96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -68,13 +68,14 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.CounterRotatorHelper;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 0e70736..d1fd207 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -40,6 +40,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 7606526..d8e8b58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -41,8 +41,8 @@
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index c6803f7..843c84a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -40,7 +40,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index 21223c9a..cac63eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -28,10 +28,10 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index 571c839..c2f4d72a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -25,10 +25,10 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.transitTypeToString;
 
+import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
-import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
 import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
 import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
 import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
@@ -71,9 +71,9 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
new file mode 100644
index 0000000..24077a3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2020 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.wm.shell.pip2;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
+ */
+public class PipSurfaceTransactionHelper {
+    /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */
+    private final Matrix mTmpTransform = new Matrix();
+    private final float[] mTmpFloat9 = new float[9];
+    private final RectF mTmpSourceRectF = new RectF();
+    private final RectF mTmpDestinationRectF = new RectF();
+    private final Rect mTmpDestinationRect = new Rect();
+
+    private int mCornerRadius;
+    private int mShadowRadius;
+
+    public PipSurfaceTransactionHelper(Context context) {
+        onDensityOrFontScaleChanged(context);
+    }
+
+    /**
+     * Called when display size or font size of settings changed
+     *
+     * @param context the current context
+     */
+    public void onDensityOrFontScaleChanged(Context context) {
+        mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
+        mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius);
+    }
+
+    /**
+     * Operates the alpha on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+            float alpha) {
+        tx.setAlpha(leash, alpha);
+        return this;
+    }
+
+    /**
+     * Operates the crop (and position) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect destinationBounds) {
+        tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+                .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        return this;
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, RectF destinationBounds) {
+        return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds, float degrees) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, RectF destinationBounds, float degrees) {
+        mTmpSourceRectF.set(sourceBounds);
+        // We want the matrix to position the surface relative to the screen coordinates so offset
+        // the source to 0,0
+        mTmpSourceRectF.offsetTo(0, 0);
+        mTmpDestinationRectF.set(destinationBounds);
+        mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
+        mTmpTransform.postRotate(degrees,
+                mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY());
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9);
+        return this;
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
+            SurfaceControl leash, Rect sourceRectHint,
+            Rect sourceBounds, Rect destinationBounds, Rect insets,
+            boolean isInPipDirection, float fraction) {
+        mTmpDestinationRect.set(sourceBounds);
+        // Similar to {@link #scale}, we want to position the surface relative to the screen
+        // coordinates so offset the bounds to 0,0
+        mTmpDestinationRect.offsetTo(0, 0);
+        mTmpDestinationRect.inset(insets);
+        // Scale to the bounds no smaller than the destination and offset such that the top/left
+        // of the scaled inset source rect aligns with the top/left of the destination bounds
+        final float scale;
+        if (isInPipDirection
+                && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
+            // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
+            final float endScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceRectHint.width()
+                    : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - fraction) * startScale + fraction * endScale;
+        } else {
+            scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+                    (float) destinationBounds.height() / sourceBounds.height());
+        }
+        final float left = destinationBounds.left - insets.left * scale;
+        final float top = destinationBounds.top - insets.top * scale;
+        mTmpTransform.setScale(scale, scale);
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+                .setCrop(leash, mTmpDestinationRect)
+                .setPosition(leash, left, top);
+        return this;
+    }
+
+    /**
+     * Operates the rotation according to the given degrees and scale (setMatrix) according to the
+     * source bounds and rotated destination bounds. The crop will be the unscaled source bounds.
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx,
+            SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, Rect insets,
+            float degrees, float positionX, float positionY, boolean isExpanding,
+            boolean clockwise) {
+        mTmpDestinationRect.set(sourceBounds);
+        mTmpDestinationRect.inset(insets);
+        final int srcW = mTmpDestinationRect.width();
+        final int srcH = mTmpDestinationRect.height();
+        final int destW = destinationBounds.width();
+        final int destH = destinationBounds.height();
+        // Scale by the short side so there won't be empty area if the aspect ratio of source and
+        // destination are different.
+        final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
+        final Rect crop = mTmpDestinationRect;
+        crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH
+                : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH);
+        // Inverse scale for crop to fit in screen coordinates.
+        crop.scale(1 / scale);
+        crop.offset(insets.left, insets.top);
+        if (isExpanding) {
+            // Expand bounds (shrink insets) in source orientation.
+            positionX -= insets.left * scale;
+            positionY -= insets.top * scale;
+        } else {
+            // Shrink bounds (expand insets) in destination orientation.
+            if (clockwise) {
+                positionX -= insets.top * scale;
+                positionY += insets.left * scale;
+            } else {
+                positionX += insets.top * scale;
+                positionY -= insets.left * scale;
+            }
+        }
+        mTmpTransform.setScale(scale, scale);
+        mTmpTransform.postRotate(degrees);
+        mTmpTransform.postTranslate(positionX, positionY);
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop);
+        return this;
+    }
+
+    /**
+     * Resets the scale (setMatrix) on a given transaction and leash if there's any
+     *
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx,
+            SurfaceControl leash,
+            Rect destinationBounds) {
+        tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9)
+                .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        return this;
+    }
+
+    /**
+     * Operates the round corner radius on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+            boolean applyCornerRadius) {
+        tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0);
+        return this;
+    }
+
+    /**
+     * Operates the round corner radius on a given transaction and leash, scaled by bounds
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect fromBounds, Rect toBounds) {
+        final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height())
+                / Math.hypot(toBounds.width(), toBounds.height()));
+        tx.setCornerRadius(leash, mCornerRadius * scale);
+        return this;
+    }
+
+    /**
+     * Operates the shadow radius on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash,
+            boolean applyShadowRadius) {
+        tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0);
+        return this;
+    }
+
+    /**
+     * Interface to standardize {@link SurfaceControl.Transaction} generation across PiP.
+     */
+    public interface SurfaceControlTransactionFactory {
+        /**
+         * @return a new transaction to operate on.
+         */
+        SurfaceControl.Transaction getTransaction();
+    }
+
+    /**
+     * Implementation of {@link SurfaceControlTransactionFactory} that returns
+     * {@link SurfaceControl.Transaction} with VsyncId being set.
+     */
+    public static class VsyncSurfaceControlTransactionFactory
+            implements SurfaceControlTransactionFactory {
+        @Override
+        public SurfaceControl.Transaction getTransaction() {
+            final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+            tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+            return tx;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
new file mode 100644
index 0000000..2478252
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2020 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.wm.shell.pip2.phone;
+
+import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Size;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the PiP menu view which can show menu options or a scrim.
+ *
+ * The current media session provides actions whenever there are no valid actions provided by the
+ * current PiP activity. Otherwise, those actions always take precedence.
+ */
+public class PhonePipMenuController implements PipMenuController {
+
+    private static final String TAG = "PhonePipMenuController";
+    private static final boolean DEBUG = false;
+
+    public static final int MENU_STATE_NONE = 0;
+    public static final int MENU_STATE_FULL = 1;
+
+    /**
+     * A listener interface to receive notification on changes in PIP.
+     */
+    public interface Listener {
+        /**
+         * Called when the PIP menu visibility change has started.
+         *
+         * @param menuState the new, about-to-change state of the menu
+         * @param resize whether or not to resize the PiP with the state change
+         */
+        void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback);
+
+        /**
+         * Called when the PIP menu state has finished changing/animating.
+         *
+         * @param menuState the new state of the menu.
+         */
+        void onPipMenuStateChangeFinish(int menuState);
+
+        /**
+         * Called when the PIP requested to be expanded.
+         */
+        void onPipExpand();
+
+        /**
+         * Called when the PIP requested to be dismissed.
+         */
+        void onPipDismiss();
+
+        /**
+         * Called when the PIP requested to show the menu.
+         */
+        void onPipShowMenu();
+
+        /**
+         * Called when the PIP requested to enter Split.
+         */
+        void onEnterSplit();
+    }
+
+    private final Matrix mMoveTransform = new Matrix();
+    private final Rect mTmpSourceBounds = new Rect();
+    private final RectF mTmpSourceRectF = new RectF();
+    private final RectF mTmpDestinationRectF = new RectF();
+    private final Context mContext;
+    private final PipBoundsState mPipBoundsState;
+    private final PipMediaController mMediaController;
+    private final ShellExecutor mMainExecutor;
+    private final Handler mMainHandler;
+
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final float[] mTmpTransform = new float[9];
+
+    private final ArrayList<Listener> mListeners = new ArrayList<>();
+    private final SystemWindows mSystemWindows;
+    private final PipUiEventLogger mPipUiEventLogger;
+
+    private List<RemoteAction> mAppActions;
+    private RemoteAction mCloseAction;
+    private List<RemoteAction> mMediaActions;
+
+    private int mMenuState;
+
+    private PipMenuView mPipMenuView;
+
+    private SurfaceControl mLeash;
+
+    private ActionListener mMediaActionListener = new ActionListener() {
+        @Override
+        public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
+            mMediaActions = new ArrayList<>(mediaActions);
+            updateMenuActions();
+        }
+    };
+
+    public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
+            PipMediaController mediaController, SystemWindows systemWindows,
+            PipUiEventLogger pipUiEventLogger,
+            ShellExecutor mainExecutor, Handler mainHandler) {
+        mContext = context;
+        mPipBoundsState = pipBoundsState;
+        mMediaController = mediaController;
+        mSystemWindows = systemWindows;
+        mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
+        mPipUiEventLogger = pipUiEventLogger;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+    }
+
+    public boolean isMenuVisible() {
+        return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
+    }
+
+    /**
+     * Attach the menu when the PiP task first appears.
+     */
+    @Override
+    public void attach(SurfaceControl leash) {
+        mLeash = leash;
+        attachPipMenuView();
+    }
+
+    /**
+     * Detach the menu when the PiP task is gone.
+     */
+    @Override
+    public void detach() {
+        hideMenu();
+        detachPipMenuView();
+        mLeash = null;
+    }
+
+    void attachPipMenuView() {
+        // In case detach was not called (e.g. PIP unexpectedly closed)
+        if (mPipMenuView != null) {
+            detachPipMenuView();
+        }
+        mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
+                mPipUiEventLogger);
+        mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {
+                v.getViewRootImpl().addSurfaceChangedCallback(
+                        new ViewRootImpl.SurfaceChangedCallback() {
+                            @Override
+                            public void surfaceCreated(SurfaceControl.Transaction t) {
+                                final SurfaceControl sc = getSurfaceControl();
+                                if (sc != null) {
+                                    t.reparent(sc, mLeash);
+                                    // make menu on top of the surface
+                                    t.setLayer(sc, Integer.MAX_VALUE);
+                                }
+                            }
+
+                            @Override
+                            public void surfaceReplaced(SurfaceControl.Transaction t) {
+                            }
+
+                            @Override
+                            public void surfaceDestroyed() {
+                            }
+                        });
+            }
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+            }
+        });
+
+        mSystemWindows.addView(mPipMenuView,
+                getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
+                0, SHELL_ROOT_LAYER_PIP);
+        setShellRootAccessibilityWindow();
+
+        // Make sure the initial actions are set
+        updateMenuActions();
+    }
+
+    private void detachPipMenuView() {
+        if (mPipMenuView == null) {
+            return;
+        }
+
+        mSystemWindows.removeView(mPipMenuView);
+        mPipMenuView = null;
+    }
+
+    /**
+     * Updates the layout parameters of the menu.
+     * @param destinationBounds New Menu bounds.
+     */
+    @Override
+    public void updateMenuBounds(Rect destinationBounds) {
+        mSystemWindows.updateViewLayout(mPipMenuView,
+                getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
+                        destinationBounds.height()));
+        updateMenuLayout(destinationBounds);
+    }
+
+    @Override
+    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (mPipMenuView != null) {
+            mPipMenuView.onFocusTaskChanged(taskInfo);
+        }
+    }
+
+    /**
+     * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some
+     * reason (ie. the window isn't ready yet, thus {@link ViewRootImpl} is
+     * {@code null}), it will get the leash that the WindowlessWM has assigned to it.
+     */
+    public SurfaceControl getSurfaceControl() {
+        return mSystemWindows.getViewSurface(mPipMenuView);
+    }
+
+    /**
+     * Adds a new menu activity listener.
+     */
+    public void addListener(Listener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    @Nullable
+    Size getEstimatedMinMenuSize() {
+        return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize();
+    }
+
+    /**
+     * When other components requests the menu controller directly to show the menu, we must
+     * first fire off the request to the other listeners who will then propagate the call
+     * back to the controller with the right parameters.
+     */
+    @Override
+    public void showMenu() {
+        mListeners.forEach(Listener::onPipShowMenu);
+    }
+
+    /**
+     * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
+     * upon PiP window transition is finished.
+     */
+    public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean showResizeHandle) {
+        if (willResizeMenu) {
+            // hide all visible controls including close button and etc. first, this is to ensure
+            // menu is totally invisible during the transition to eliminate unpleasant artifacts
+            fadeOutMenu();
+        }
+        showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+                willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle);
+    }
+
+    /**
+     * Shows the menu activity immediately.
+     */
+    public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean showResizeHandle) {
+        showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+                false /* withDelay */, showResizeHandle);
+    }
+
+    private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: showMenu() state=%s"
+                            + " isMenuVisible=%s"
+                            + " allowMenuTimeout=%s"
+                            + " willResizeMenu=%s"
+                            + " withDelay=%s"
+                            + " showResizeHandle=%s"
+                            + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout,
+                    willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // Sync the menu bounds before showing it in case it is out of sync.
+        movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
+                PipMenuController.ALPHA_NO_CHANGE);
+        updateMenuBounds(stackBounds);
+
+        mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
+                showResizeHandle);
+    }
+
+    /**
+     * Move the PiP menu, which does a translation and possibly a scale transformation.
+     */
+    @Override
+    public void movePipMenu(@Nullable SurfaceControl pipLeash,
+            @Nullable SurfaceControl.Transaction t,
+            Rect destinationBounds, float alpha) {
+        if (destinationBounds.isEmpty()) {
+            return;
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+        if (pipLeash != null && t != null) {
+            t.apply();
+        }
+    }
+
+    /**
+     * Does an immediate window crop of the PiP menu.
+     */
+    @Override
+    public void resizePipMenu(@Nullable SurfaceControl pipLeash,
+            @Nullable SurfaceControl.Transaction t,
+            Rect destinationBounds) {
+        if (destinationBounds.isEmpty()) {
+            return;
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+        if (pipLeash != null && t != null) {
+            t.apply();
+        }
+    }
+
+    private boolean checkPipMenuState() {
+        if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Pokes the menu, indicating that the user is interacting with it.
+     */
+    public void pokeMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible);
+        }
+        if (isMenuVisible) {
+            mPipMenuView.pokeMenu();
+        }
+    }
+
+    private void fadeOutMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible);
+        }
+        if (isMenuVisible) {
+            mPipMenuView.fadeOutMenu();
+        }
+    }
+
+    /**
+     * Hides the menu view.
+     */
+    public void hideMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (isMenuVisible) {
+            mPipMenuView.hideMenu();
+        }
+    }
+
+    /**
+     * Hides the menu view.
+     *
+     * @param animationType the animation type to use upon hiding the menu
+     * @param resize whether or not to resize the PiP with the state change
+     */
+    public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: hideMenu() state=%s"
+                            + " isMenuVisible=%s"
+                            + " animationType=%s"
+                            + " resize=%s"
+                            + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+                    animationType, resize,
+                    Debug.getCallers(5, "    "));
+        }
+        if (isMenuVisible) {
+            mPipMenuView.hideMenu(resize, animationType);
+        }
+    }
+
+    /**
+     * Hides the menu activity.
+     */
+    public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
+        if (isMenuVisible()) {
+            // If the menu is visible in either the closed or full state, then hide the menu and
+            // trigger the animation trigger afterwards
+            if (onStartCallback != null) {
+                onStartCallback.run();
+            }
+            mPipMenuView.hideMenu(onEndCallback);
+        }
+    }
+
+    /**
+     * Sets the menu actions to the actions provided by the current PiP menu.
+     */
+    @Override
+    public void setAppActions(List<RemoteAction> appActions,
+            RemoteAction closeAction) {
+        mAppActions = appActions;
+        mCloseAction = closeAction;
+        updateMenuActions();
+    }
+
+    void onPipExpand() {
+        mListeners.forEach(Listener::onPipExpand);
+    }
+
+    void onPipDismiss() {
+        mListeners.forEach(Listener::onPipDismiss);
+    }
+
+    void onEnterSplit() {
+        mListeners.forEach(Listener::onEnterSplit);
+    }
+
+    /**
+     * @return the best set of actions to show in the PiP menu.
+     */
+    private List<RemoteAction> resolveMenuActions() {
+        if (isValidActions(mAppActions)) {
+            return mAppActions;
+        }
+        return mMediaActions;
+    }
+
+    /**
+     * Updates the PiP menu with the best set of actions provided.
+     */
+    private void updateMenuActions() {
+        if (mPipMenuView != null) {
+            mPipMenuView.setActions(mPipBoundsState.getBounds(),
+                    resolveMenuActions(), mCloseAction);
+        }
+    }
+
+    /**
+     * Returns whether the set of actions are valid.
+     */
+    private static boolean isValidActions(List<?> actions) {
+        return actions != null && actions.size() > 0;
+    }
+
+    /**
+     * Handles changes in menu visibility.
+     */
+    void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: onMenuStateChangeStart() mMenuState=%s"
+                            + " menuState=%s resize=%s"
+                            + " callers=\n%s", TAG, mMenuState, menuState, resize,
+                    Debug.getCallers(5, "    "));
+        }
+
+        if (menuState != mMenuState) {
+            mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback));
+            if (menuState == MENU_STATE_FULL) {
+                // Once visible, start listening for media action changes. This call will trigger
+                // the menu actions to be updated again.
+                mMediaController.addActionListener(mMediaActionListener);
+            } else {
+                // Once hidden, stop listening for media action changes. This call will trigger
+                // the menu actions to be updated again.
+                mMediaController.removeActionListener(mMediaActionListener);
+            }
+
+            try {
+                WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
+                        mSystemWindows.getFocusGrantToken(mPipMenuView),
+                        menuState != MENU_STATE_NONE /* grantFocus */);
+            } catch (RemoteException e) {
+                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: Unable to update focus as menu appears/disappears, %s", TAG, e);
+            }
+        }
+    }
+
+    void onMenuStateChangeFinish(int menuState) {
+        if (menuState != mMenuState) {
+            mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
+        }
+        mMenuState = menuState;
+        setShellRootAccessibilityWindow();
+    }
+
+    private void setShellRootAccessibilityWindow() {
+        switch (mMenuState) {
+            case MENU_STATE_NONE:
+                mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
+                break;
+            default:
+                mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP,
+                        mPipMenuView);
+                break;
+        }
+    }
+
+    /**
+     * Handles a pointer event sent from pip input consumer.
+     */
+    void handlePointerEvent(MotionEvent ev) {
+        if (mPipMenuView == null) {
+            return;
+        }
+
+        if (ev.isTouchEvent()) {
+            mPipMenuView.dispatchTouchEvent(ev);
+        } else {
+            mPipMenuView.dispatchGenericMotionEvent(ev);
+        }
+    }
+
+    /**
+     * Tell the PIP Menu to recalculate its layout given its current position on the display.
+     */
+    public void updateMenuLayout(Rect bounds) {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: updateMenuLayout() state=%s"
+                            + " isMenuVisible=%s"
+                            + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+                    Debug.getCallers(5, "    "));
+        }
+        if (isMenuVisible) {
+            mPipMenuView.updateMenuLayout(bounds);
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mMenuState=" + mMenuState);
+        pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
+        pw.println(innerPrefix + "mListeners=" + mListeners.size());
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
new file mode 100644
index 0000000..7252675
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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.wm.shell.pip2.phone;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Container layout wraps single action image view drawn in PiP menu and can restrict the size of
+ * action image view (see pip_menu_action.xml).
+ */
+public class PipMenuActionView extends FrameLayout {
+    private ImageView mImageView;
+    private View mCustomCloseBackground;
+
+    public PipMenuActionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mImageView = findViewById(R.id.image);
+        mCustomCloseBackground = findViewById(R.id.custom_close_bg);
+    }
+
+    /** pass through to internal {@link #mImageView} */
+    public void setImageDrawable(Drawable drawable) {
+        mImageView.setImageDrawable(drawable);
+    }
+
+    /** pass through to internal {@link #mCustomCloseBackground} */
+    public void setCustomCloseBackgroundVisibility(@Visibility int visibility) {
+        mCustomCloseBackground.setVisibility(visibility);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
new file mode 100644
index 0000000..b5e575b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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.wm.shell.pip2.phone;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Helper class to calculate and place the menu icons on the PIP Menu.
+ */
+public class PipMenuIconsAlgorithm {
+
+    private static final String TAG = "PipMenuIconsAlgorithm";
+
+    protected ViewGroup mViewRoot;
+    protected ViewGroup mTopEndContainer;
+    protected View mDragHandle;
+    protected View mEnterSplitButton;
+    protected View mSettingsButton;
+    protected View mDismissButton;
+
+    protected PipMenuIconsAlgorithm(Context context) {
+    }
+
+    /**
+     * Bind the necessary views.
+     */
+    public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle,
+            View enterSplitButton, View settingsButton, View dismissButton) {
+        mViewRoot = viewRoot;
+        mTopEndContainer = topEndContainer;
+        mDragHandle = dragHandle;
+        mEnterSplitButton = enterSplitButton;
+        mSettingsButton = settingsButton;
+        mDismissButton = dismissButton;
+    }
+
+    /**
+     * Updates the position of the drag handle based on where the PIP window is on the screen.
+     */
+    public void onBoundsChanged(Rect bounds) {
+        // On phones, the menu icons are always static and will never move based on the PIP window
+        // position. No need to do anything here.
+    }
+
+    /**
+     * Set the gravity on the given view.
+     */
+    protected static void setLayoutGravity(View v, int gravity) {
+        if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) {
+            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams();
+            params.gravity = gravity;
+            v.setLayoutParams(params);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
new file mode 100644
index 0000000..a5b76c7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2020 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.wm.shell.pip2.phone;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Pair;
+import android.util.Size;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Translucent window that gets started on top of a task in PIP to allow the user to control it.
+ */
+public class PipMenuView extends FrameLayout {
+
+    private static final String TAG = "PipMenuView";
+
+    private static final int ANIMATION_NONE_DURATION_MS = 0;
+    private static final int ANIMATION_HIDE_DURATION_MS = 125;
+
+    /** No animation performed during menu hide. */
+    public static final int ANIM_TYPE_NONE = 0;
+    /** Fade out the menu until it's invisible. Used when the PIP window remains visible.  */
+    public static final int ANIM_TYPE_HIDE = 1;
+    /** Fade out the menu in sync with the PIP window. */
+    public static final int ANIM_TYPE_DISMISS = 2;
+
+    @IntDef(prefix = { "ANIM_TYPE_" }, value = {
+            ANIM_TYPE_NONE,
+            ANIM_TYPE_HIDE,
+            ANIM_TYPE_DISMISS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimationType {}
+
+    private static final int INITIAL_DISMISS_DELAY = 3500;
+    private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
+    private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30;
+
+    private static final float MENU_BACKGROUND_ALPHA = 0.54f;
+    private static final float DISABLED_ACTION_ALPHA = 0.54f;
+
+    private int mMenuState;
+    private boolean mAllowMenuTimeout = true;
+    private boolean mAllowTouches = true;
+    private int mDismissFadeOutDurationMs;
+    private final List<RemoteAction> mActions = new ArrayList<>();
+    private RemoteAction mCloseAction;
+
+    private AccessibilityManager mAccessibilityManager;
+    private Drawable mBackgroundDrawable;
+    private View mMenuContainer;
+    private LinearLayout mActionsGroup;
+    private int mBetweenActionPaddingLand;
+
+    private AnimatorSet mMenuContainerAnimator;
+    private final PhonePipMenuController mController;
+    private final PipUiEventLogger mPipUiEventLogger;
+
+    private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float alpha = (float) animation.getAnimatedValue();
+                    mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255));
+                }
+            };
+
+    private ShellExecutor mMainExecutor;
+    private Handler mMainHandler;
+
+    /**
+     * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too
+     * small and it is resized on menu show to fit the actions.
+     */
+    private boolean mDidLastShowMenuResize;
+    private final Runnable mHideMenuRunnable = this::hideMenu;
+
+    protected View mViewRoot;
+    protected View mSettingsButton;
+    protected View mDismissButton;
+    protected View mEnterSplitButton;
+    protected View mTopEndContainer;
+    protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm;
+
+    // How long the shell will wait for the app to close the PiP if a custom action is set.
+    private final int mPipForceCloseDelay;
+
+    public PipMenuView(Context context, PhonePipMenuController controller,
+            ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) {
+        super(context, null, 0);
+        mContext = context;
+        mController = controller;
+        mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
+        mPipUiEventLogger = pipUiEventLogger;
+
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+        inflate(context, R.layout.pip_menu, this);
+
+        mPipForceCloseDelay = context.getResources().getInteger(
+                R.integer.config_pipForceCloseDelay);
+
+        mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background);
+        mBackgroundDrawable.setAlpha(0);
+        mViewRoot = findViewById(R.id.background);
+        mViewRoot.setBackground(mBackgroundDrawable);
+        mMenuContainer = findViewById(R.id.menu_container);
+        mMenuContainer.setAlpha(0);
+        mTopEndContainer = findViewById(R.id.top_end_container);
+        mSettingsButton = findViewById(R.id.settings);
+        mSettingsButton.setAlpha(0);
+        mSettingsButton.setOnClickListener((v) -> {
+            if (v.getAlpha() != 0) {
+                showSettings();
+            }
+        });
+        mDismissButton = findViewById(R.id.dismiss);
+        mDismissButton.setAlpha(0);
+        mDismissButton.setOnClickListener(v -> dismissPip());
+        findViewById(R.id.expand_button).setOnClickListener(v -> {
+            if (mMenuContainer.getAlpha() != 0) {
+                expandPip();
+            }
+        });
+
+        mEnterSplitButton = findViewById(R.id.enter_split);
+        mEnterSplitButton.setAlpha(0);
+        mEnterSplitButton.setOnClickListener(v -> {
+            if (mEnterSplitButton.getAlpha() != 0) {
+                enterSplit();
+            }
+        });
+
+        // this disables the ripples
+        mEnterSplitButton.setEnabled(false);
+
+        findViewById(R.id.resize_handle).setAlpha(0);
+
+        mActionsGroup = findViewById(R.id.actions_group);
+        mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
+                R.dimen.pip_between_action_padding_land);
+        mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext);
+        mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer,
+                findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton,
+                mDismissButton);
+        mDismissFadeOutDurationMs = context.getResources()
+                .getInteger(R.integer.config_pipExitAnimationDuration);
+
+        initAccessibility();
+    }
+
+    private void initAccessibility() {
+        this.setAccessibilityDelegate(new AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                String label = getResources().getString(R.string.pip_menu_title);
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label));
+            }
+
+            @Override
+            public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) {
+                    mController.showMenu();
+                }
+                return super.performAccessibilityAction(host, action, args);
+            }
+        });
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+            hideMenu();
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (!mAllowTouches) {
+            return false;
+        }
+
+        if (mAllowMenuTimeout) {
+            repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+        }
+
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent event) {
+        if (mAllowMenuTimeout) {
+            repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+        }
+
+        return super.dispatchGenericMotionEvent(event);
+    }
+
+    void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {}
+
+    void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) {
+        mAllowMenuTimeout = allowMenuTimeout;
+        mDidLastShowMenuResize = resizeMenuOnShow;
+        final boolean enableEnterSplit =
+                mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton);
+        if (mMenuState != menuState) {
+            // Disallow touches if the menu needs to resize while showing, and we are transitioning
+            // to/from a full menu state.
+            boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow
+                    && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
+            mAllowTouches = !disallowTouchesUntilAnimationEnd;
+            cancelDelayedHide();
+            if (mMenuContainerAnimator != null) {
+                mMenuContainerAnimator.cancel();
+            }
+            mMenuContainerAnimator = new AnimatorSet();
+            ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+                    mMenuContainer.getAlpha(), 1f);
+            menuAnim.addUpdateListener(mMenuBgUpdateListener);
+            ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+                    mSettingsButton.getAlpha(), 1f);
+            ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+                    mDismissButton.getAlpha(), 1f);
+            if (menuState == MENU_STATE_FULL) {
+                mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
+            }
+            mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
+            mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS);
+            mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAllowTouches = true;
+                    notifyMenuStateChangeFinish(menuState);
+                    if (allowMenuTimeout) {
+                        repostDelayedHide(INITIAL_DISMISS_DELAY);
+                    }
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    mAllowTouches = true;
+                }
+            });
+            if (withDelay) {
+                // starts the menu container animation after window expansion is completed
+                notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> {
+                    if (mMenuContainerAnimator == null) {
+                        return;
+                    }
+                    mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY);
+                    setVisibility(VISIBLE);
+                    mMenuContainerAnimator.start();
+                });
+            } else {
+                notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null);
+                setVisibility(VISIBLE);
+                mMenuContainerAnimator.start();
+            }
+            updateActionViews(menuState, stackBounds);
+        } else {
+            // If we are already visible, then just start the delayed dismiss and unregister any
+            // existing input consumers from the previous drag
+            if (allowMenuTimeout) {
+                repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+            }
+        }
+    }
+
+    /**
+     * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
+     * and instead, it fades out the controls by setting the alpha to 0 directly without menu
+     * visibility callbacks invoked.
+     */
+    void fadeOutMenu() {
+        mMenuContainer.setAlpha(0f);
+        mSettingsButton.setAlpha(0f);
+        mDismissButton.setAlpha(0f);
+        mEnterSplitButton.setAlpha(0f);
+    }
+
+    void pokeMenu() {
+        cancelDelayedHide();
+    }
+
+    void updateMenuLayout(Rect bounds) {
+        mPipMenuIconsAlgorithm.onBoundsChanged(bounds);
+    }
+
+    void hideMenu() {
+        hideMenu(null);
+    }
+
+    void hideMenu(Runnable animationEndCallback) {
+        hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize,
+                ANIM_TYPE_HIDE);
+    }
+
+    void hideMenu(boolean resize, @AnimationType int animationType) {
+        hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize,
+                animationType);
+    }
+
+    void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
+            boolean resize, @AnimationType int animationType) {
+        if (mMenuState != MENU_STATE_NONE) {
+            cancelDelayedHide();
+            if (notifyMenuVisibility) {
+                notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null);
+            }
+            mMenuContainerAnimator = new AnimatorSet();
+            ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+                    mMenuContainer.getAlpha(), 0f);
+            menuAnim.addUpdateListener(mMenuBgUpdateListener);
+            ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+                    mSettingsButton.getAlpha(), 0f);
+            ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+                    mDismissButton.getAlpha(), 0f);
+            ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA,
+                    mEnterSplitButton.getAlpha(), 0f);
+            mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
+                    enterSplitAnim);
+            mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+            mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType));
+            mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    setVisibility(GONE);
+                    if (notifyMenuVisibility) {
+                        notifyMenuStateChangeFinish(MENU_STATE_NONE);
+                    }
+                    if (animationFinishedRunnable != null) {
+                        animationFinishedRunnable.run();
+                    }
+                }
+            });
+            mMenuContainerAnimator.start();
+        }
+    }
+
+    /**
+     * @return Estimated minimum {@link Size} to hold the actions.
+     * See also {@link #updateActionViews(Rect)}
+     */
+    Size getEstimatedMinMenuSize() {
+        final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size);
+        // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button
+        // on the top action container.
+        final int width = Math.max(2, mActions.size()) * pipActionSize;
+        final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size)
+                + getResources().getDimensionPixelSize(R.dimen.pip_action_padding)
+                + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin);
+        return new Size(width, height);
+    }
+
+    void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions,
+            @Nullable RemoteAction closeAction) {
+        mActions.clear();
+        if (actions != null && !actions.isEmpty()) {
+            mActions.addAll(actions);
+        }
+        mCloseAction = closeAction;
+        if (mMenuState == MENU_STATE_FULL) {
+            updateActionViews(mMenuState, stackBounds);
+        }
+    }
+
+    private void updateActionViews(int menuState, Rect stackBounds) {
+        ViewGroup expandContainer = findViewById(R.id.expand_container);
+        ViewGroup actionsContainer = findViewById(R.id.actions_container);
+        actionsContainer.setOnTouchListener((v, ev) -> {
+            // Do nothing, prevent click through to parent
+            return true;
+        });
+
+        // Update the expand button only if it should show with the menu
+        expandContainer.setVisibility(menuState == MENU_STATE_FULL
+                ? View.VISIBLE
+                : View.INVISIBLE);
+
+        LayoutParams expandedLp =
+                (LayoutParams) expandContainer.getLayoutParams();
+        if (mActions.isEmpty() || menuState == MENU_STATE_NONE) {
+            actionsContainer.setVisibility(View.INVISIBLE);
+
+            // Update the expand container margin to adjust the center of the expand button to
+            // account for the existence of the action container
+            expandedLp.topMargin = 0;
+            expandedLp.bottomMargin = 0;
+        } else {
+            actionsContainer.setVisibility(View.VISIBLE);
+            if (mActionsGroup != null) {
+                // Ensure we have as many buttons as actions
+                final LayoutInflater inflater = LayoutInflater.from(mContext);
+                while (mActionsGroup.getChildCount() < mActions.size()) {
+                    final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate(
+                            R.layout.pip_menu_action, mActionsGroup, false);
+                    mActionsGroup.addView(actionView);
+                }
+
+                // Update the visibility of all views
+                for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
+                    mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
+                            ? View.VISIBLE
+                            : View.GONE);
+                }
+
+                // Recreate the layout
+                final boolean isLandscapePip = stackBounds != null
+                        && (stackBounds.width() > stackBounds.height());
+                for (int i = 0; i < mActions.size(); i++) {
+                    final RemoteAction action = mActions.get(i);
+                    final PipMenuActionView actionView =
+                            (PipMenuActionView) mActionsGroup.getChildAt(i);
+                    final boolean isCloseAction = mCloseAction != null && Objects.equals(
+                            mCloseAction.getActionIntent(), action.getActionIntent());
+
+                    final int iconType = action.getIcon().getType();
+                    if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+                        // Disallow loading icon from content URI
+                        actionView.setImageDrawable(null);
+                    } else {
+                        // TODO: Check if the action drawable has changed before we reload it
+                        action.getIcon().loadDrawableAsync(mContext, d -> {
+                            if (d != null) {
+                                d.setTint(Color.WHITE);
+                                actionView.setImageDrawable(d);
+                            }
+                        }, mMainHandler);
+                    }
+                    actionView.setCustomCloseBackgroundVisibility(
+                            isCloseAction ? View.VISIBLE : View.GONE);
+                    actionView.setContentDescription(action.getContentDescription());
+                    if (action.isEnabled()) {
+                        actionView.setOnClickListener(
+                                v -> onActionViewClicked(action.getActionIntent(), isCloseAction));
+                    }
+                    actionView.setEnabled(action.isEnabled());
+                    actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
+
+                    // Update the margin between actions
+                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+                            actionView.getLayoutParams();
+                    lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
+                }
+            }
+
+            // Update the expand container margin to adjust the center of the expand button to
+            // account for the existence of the action container
+            expandedLp.topMargin = getResources().getDimensionPixelSize(
+                    R.dimen.pip_action_padding);
+            expandedLp.bottomMargin = getResources().getDimensionPixelSize(
+                    R.dimen.pip_expand_container_edge_margin);
+        }
+        expandContainer.requestLayout();
+    }
+
+    private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+        mController.onMenuStateChangeStart(menuState, resize, callback);
+    }
+
+    private void notifyMenuStateChangeFinish(int menuState) {
+        mMenuState = menuState;
+        mController.onMenuStateChangeFinish(menuState);
+    }
+
+    private void expandPip() {
+        // Do not notify menu visibility when hiding the menu, the controller will do this when it
+        // handles the message
+        hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */,
+                ANIM_TYPE_HIDE);
+        mPipUiEventLogger.log(
+                PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN);
+    }
+
+    private void dismissPip() {
+        if (mMenuState != MENU_STATE_NONE) {
+            // Do not call hideMenu() directly. Instead, let the menu controller handle it just as
+            // any other dismissal that will update the touch state and fade out the PIP task
+            // and the menu view at the same time.
+            mController.onPipDismiss();
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE);
+        }
+    }
+
+    /**
+     * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}.
+     * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure
+     * the PiP is removed after a certain timeout in case the app does not respond in a
+     * timely manner.
+     */
+    private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) {
+        try {
+            intent.send();
+        } catch (PendingIntent.CanceledException e) {
+            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Failed to send action, %s", TAG, e);
+        }
+        if (isCloseAction) {
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE);
+            mAllowTouches = false;
+            mMainExecutor.executeDelayed(() -> {
+                hideMenu();
+                // TODO: it's unsafe to call onPipDismiss with a delay here since
+                // we may have a different PiP by the time this runnable is executed.
+                mController.onPipDismiss();
+                mAllowTouches = true;
+            }, mPipForceCloseDelay);
+        }
+    }
+
+    private void enterSplit() {
+        // Do not notify menu visibility when hiding the menu, the controller will do this when it
+        // handles the message
+        hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */,
+                ANIM_TYPE_HIDE);
+    }
+
+
+    private void showSettings() {
+        final Pair<ComponentName, Integer> topPipActivityInfo =
+                PipUtils.getTopPipActivity(mContext);
+        if (topPipActivityInfo.first != null) {
+            final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
+                    Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
+            settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+            mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second));
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS);
+        }
+    }
+
+    private void cancelDelayedHide() {
+        mMainExecutor.removeCallbacks(mHideMenuRunnable);
+    }
+
+    private void repostDelayedHide(int delay) {
+        int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
+                FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
+        mMainExecutor.removeCallbacks(mHideMenuRunnable);
+        mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout);
+    }
+
+    private long getFadeOutDuration(@AnimationType int animationType) {
+        switch (animationType) {
+            case ANIM_TYPE_NONE:
+                return ANIMATION_NONE_DURATION_MS;
+            case ANIM_TYPE_HIDE:
+                return ANIMATION_HIDE_DURATION_MS;
+            case ANIM_TYPE_DISMISS:
+                return mDismissFadeOutDurationMs;
+            default:
+                throw new IllegalStateException("Invalid animation type " + animationType);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index f3d178a..fbf4d13 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -42,8 +42,8 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 1232baa..97d3457 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -58,10 +58,10 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.HomeTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 5de8a9b..e8894a83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -48,9 +48,9 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitDecorManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.transition.OneShotRemoteHandler;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index af05aa2..e5045ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -44,6 +44,8 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
 import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
@@ -64,8 +66,6 @@
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
-import static com.android.wm.shell.util.TransitionUtil.isClosingType;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -135,13 +135,13 @@
 import com.android.wm.shell.common.split.SplitWindowManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
 import com.android.wm.shell.transition.DefaultMixedHandler;
 import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.util.SplitBounds;
-import com.android.wm.shell.util.TransitionUtil;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import dalvik.annotation.optimization.NeverCompile;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 84f21f6..198ec82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -37,8 +37,8 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.Objects;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index 628ce27..b03daaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -27,8 +27,8 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.wm.shell.util.CounterRotator;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.CounterRotator;
+import com.android.wm.shell.shared.TransitionUtil;
 
 import java.util.List;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 8c2203e..8746b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -24,7 +24,7 @@
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -48,11 +48,11 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.StageCoordinator;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.UnfoldTransitionHandler;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.Map;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 193a4fb..c70a821 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -109,8 +109,8 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index af31f5f..cb2944c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -32,7 +32,7 @@
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 /**
  * The {@link TransitionObserver} that observes for transitions involving the home
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index 293b660..4c4c580 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -39,7 +39,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 import java.util.ArrayList;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index b012d35..1be85d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -56,7 +56,7 @@
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 /** The helper class that provides methods for adding styles to transition animations. */
 public class TransitionAnimationHelper {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 67fc7e2..5e79681 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -37,9 +37,9 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
-import static com.android.wm.shell.util.TransitionUtil.isClosingType;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -80,13 +80,13 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.tracing.LegacyTransitionTracer;
 import com.android.wm.shell.transition.tracing.PerfettoTransitionTracer;
 import com.android.wm.shell.transition.tracing.TransitionTracer;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 98d343b..c26604a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -35,6 +35,7 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
@@ -43,7 +44,6 @@
 import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
 import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 4ba05ce..a8b39c41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -345,7 +345,7 @@
                 mTaskOperations.injectBackKey();
             } else if (id == R.id.caption_handle || id == R.id.open_menu_button) {
                 if (!decoration.isHandleMenuActive()) {
-                    moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+                    moveTaskToFront(decoration.mTaskInfo);
                     decoration.createHandleMenu();
                 } else {
                     decoration.closeHandleMenu();
@@ -419,10 +419,10 @@
                     && id != R.id.maximize_window) {
                 return false;
             }
-            moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            moveTaskToFront(decoration.mTaskInfo);
 
             if (!mHasLongClicked && id != R.id.maximize_window) {
-                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
                 decoration.closeMaximizeMenuIfNeeded(e);
             }
 
@@ -466,7 +466,8 @@
          */
         @Override
         public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
-            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            final RunningTaskInfo taskInfo = decoration.mTaskInfo;
             if (DesktopModeStatus.isEnabled()
                     && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
                 return false;
@@ -492,8 +493,6 @@
                 }
                 case MotionEvent.ACTION_MOVE: {
                     mShouldClick = false;
-                    final DesktopModeWindowDecoration decoration =
-                            mWindowDecorByTaskId.get(mTaskId);
                     // If a decor's resize drag zone is active, don't also try to reposition it.
                     if (decoration.isHandlingDragResize()) break;
                     decoration.closeMaximizeMenu();
@@ -557,9 +556,10 @@
                     && action != MotionEvent.ACTION_CANCEL)) {
                 return false;
             }
-            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
-            mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo,
-                    mWindowDecorByTaskId.get(taskInfo.taskId)));
+            mDesktopTasksController.ifPresent(c -> {
+                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+                c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration);
+            });
             return true;
         }
     }
@@ -843,7 +843,18 @@
 
     @Nullable
     private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
-        if (mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible()) {
+        final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+        if (focusedDecor == null) {
+            return null;
+        }
+        final boolean splitScreenVisible = mSplitScreenController != null
+                && mSplitScreenController.isSplitScreenVisible();
+        // It's possible that split tasks are visible but neither is focused, such as when there's
+        // a fullscreen translucent window on top of them. In that case, the relevant decor should
+        // just be that translucent focused window.
+        final boolean focusedTaskInSplit = mSplitScreenController != null
+                && mSplitScreenController.isTaskInSplitScreen(focusedDecor.mTaskInfo.taskId);
+        if (splitScreenVisible && focusedTaskInSplit) {
             // We can't look at focused task here as only one task will have focus.
             DesktopModeWindowDecoration splitTaskDecor = getSplitScreenDecor(ev);
             return splitTaskDecor == null ? getFocusedDecor() : splitTaskDecor;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index b3ffd2f..3f0a281 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -36,7 +36,6 @@
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.util.Log;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
@@ -392,23 +391,16 @@
     }
 
     private void loadAppInfo() {
-        String packageName = mTaskInfo.realActivity.getPackageName();
         PackageManager pm = mContext.getApplicationContext().getPackageManager();
-        try {
-            final IconProvider provider = new IconProvider(mContext);
-            mAppIconDrawable = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
-                    PackageManager.ComponentInfoFlags.of(0)));
-            final Resources resources = mContext.getResources();
-            final BaseIconFactory factory = new BaseIconFactory(mContext,
-                    resources.getDisplayMetrics().densityDpi,
-                    resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
-            mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
-            final ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
-                    PackageManager.ApplicationInfoFlags.of(0));
-            mAppName = pm.getApplicationLabel(applicationInfo);
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Package not found: " + packageName, e);
-        }
+        final IconProvider provider = new IconProvider(mContext);
+        mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo);
+        final Resources resources = mContext.getResources();
+        final BaseIconFactory factory = new BaseIconFactory(mContext,
+                resources.getDisplayMetrics().densityDpi,
+                resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
+        mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
+        final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo;
+        mAppName = pm.getApplicationLabel(applicationInfo);
     }
 
     private void closeDragResizeListener() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
index 368231e..b0d3b50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -125,6 +125,7 @@
 
         relayout(taskBounds, t);
         if (fadeIn) {
+            cancelAnimation();
             mVeilAnimator = new ValueAnimator();
             mVeilAnimator.setFloatValues(0f, 1f);
             mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
@@ -210,15 +211,16 @@
      * Animate veil's alpha to 0, fading it out.
      */
     public void hideVeil() {
-        final ValueAnimator animator = new ValueAnimator();
-        animator.setFloatValues(1, 0);
-        animator.setDuration(RESIZE_ALPHA_DURATION);
-        animator.addUpdateListener(animation -> {
+        cancelAnimation();
+        mVeilAnimator = new ValueAnimator();
+        mVeilAnimator.setFloatValues(1, 0);
+        mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
+        mVeilAnimator.addUpdateListener(animation -> {
             SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
-            t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction());
+            t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction());
             t.apply();
         });
-        animator.addListener(new AnimatorListenerAdapter() {
+        mVeilAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
@@ -226,7 +228,7 @@
                 t.apply();
             }
         });
-        animator.start();
+        mVeilAnimator.start();
     }
 
     @ColorRes
@@ -240,10 +242,20 @@
         }
     }
 
+    private void cancelAnimation() {
+        if (mVeilAnimator != null) {
+            mVeilAnimator.removeAllUpdateListeners();
+            mVeilAnimator.cancel();
+        }
+    }
+
     /**
      * Dispose of veil when it is no longer needed, likely on close of its container decor.
      */
     void dispose() {
+        cancelAnimation();
+        mVeilAnimator = null;
+
         if (mViewHost != null) {
             mViewHost.release();
             mViewHost = null;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index dab762f..fa0aba5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -1190,6 +1190,23 @@
         assertThat(mBubbleData.getBubbleInStackWithKey(appBubbleKey)).isNull();
     }
 
+    @Test
+    public void test_removeOverflowBubble() {
+        sendUpdatedEntryAtTime(mEntryA1, 2000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
+
+        mBubbleData.removeOverflowBubble(mBubbleA1);
+        verifyUpdateReceived();
+
+        BubbleData.Update update = mUpdateCaptor.getValue();
+        assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1);
+        assertOverflowChangedTo(ImmutableList.of());
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 4878df8..75965d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -56,9 +56,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-/**
- * Tests for loading / inflating views & icons for a bubble.
- */
+/** Tests for loading / inflating views & icons for a bubble. */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
@@ -76,25 +74,33 @@
     @Before
     fun setup() {
         metadataFlagListener = Bubbles.BubbleMetadataFlagListener {}
-        iconFactory = BubbleIconFactory(context,
+        iconFactory =
+            BubbleIconFactory(
+                context,
                 60,
                 30,
                 Color.RED,
-                mContext.resources.getDimensionPixelSize(
-                        R.dimen.importance_ring_stroke_width))
+                mContext.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width)
+            )
 
         mainExecutor = TestShellExecutor()
         val windowManager = context.getSystemService(WindowManager::class.java)
         val shellInit = ShellInit(mainExecutor)
         val shellCommandHandler = ShellCommandHandler()
-        val shellController = ShellController(context, shellInit, shellCommandHandler,
-                mainExecutor)
+        val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor)
         val bubblePositioner = BubblePositioner(context, windowManager)
-        val bubbleData = BubbleData(context, mock<BubbleLogger>(), bubblePositioner,
-                BubbleEducationController(context), mainExecutor)
+        val bubbleData =
+            BubbleData(
+                context,
+                mock<BubbleLogger>(),
+                bubblePositioner,
+                BubbleEducationController(context),
+                mainExecutor
+            )
         val surfaceSynchronizer = { obj: Runnable -> obj.run() }
 
-        bubbleController = BubbleController(
+        bubbleController =
+            BubbleController(
                 context,
                 shellInit,
                 shellCommandHandler,
@@ -122,18 +128,36 @@
                 mock<Transitions>(),
                 mock<SyncTransactionQueue>(),
                 mock<IWindowManager>(),
-                mock<BubbleProperties>())
+                mock<BubbleProperties>()
+            )
 
-        bubbleStackView = BubbleStackView(context, bubbleController, bubbleData,
-                surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor)
+        val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
+        bubbleStackView =
+            BubbleStackView(
+                context,
+                bubbleStackViewManager,
+                bubblePositioner,
+                bubbleData,
+                surfaceSynchronizer,
+                FloatingContentCoordinator(),
+                bubbleController,
+                mainExecutor
+            )
         bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
     }
 
     @Test
     fun testPopulate() {
         bubble = createBubbleWithShortcut()
-        val info = BubbleViewInfoTask.BubbleViewInfo.populate(context,
-                bubbleController, bubbleStackView, iconFactory, bubble, false /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populate(
+                context,
+                bubbleController,
+                bubbleStackView,
+                iconFactory,
+                bubble,
+                false /* skipInflation */
+            )
         assertThat(info!!).isNotNull()
 
         assertThat(info.imageView).isNotNull()
@@ -151,9 +175,15 @@
     @Test
     fun testPopulateForBubbleBar() {
         bubble = createBubbleWithShortcut()
-        val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
-                bubbleController, bubbleBarLayerView, iconFactory, bubble,
-                false /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+                context,
+                bubbleController,
+                bubbleBarLayerView,
+                iconFactory,
+                bubble,
+                false /* skipInflation */
+            )
         assertThat(info!!).isNotNull()
 
         assertThat(info.imageView).isNull()
@@ -176,12 +206,18 @@
         // exception here if the app has an issue loading the shortcut icon; we default to
         // the app icon in that case / none of the icons will be null.
         val mockIconFactory = mock<BubbleIconFactory>()
-        whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo),
-                any())).doThrow(RuntimeException())
+        whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo), any()))
+            .doThrow(RuntimeException())
 
-        val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
-                bubbleController, bubbleBarLayerView, iconFactory, bubble,
-                true /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+                context,
+                bubbleController,
+                bubbleBarLayerView,
+                iconFactory,
+                bubble,
+                true /* skipInflation */
+            )
         assertThat(info).isNotNull()
 
         assertThat(info?.shortcutInfo).isNotNull()
@@ -194,8 +230,17 @@
 
     private fun createBubbleWithShortcut(): Bubble {
         val shortcutInfo = ShortcutInfo.Builder(mContext, "mockShortcutId").build()
-        return Bubble("mockKey", shortcutInfo, 1000, Resources.ID_NULL,
-                "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */,
-                mainExecutor, metadataFlagListener)
+        return Bubble(
+            "mockKey",
+            shortcutInfo,
+            1000,
+            Resources.ID_NULL,
+            "mockTitle",
+            0 /* taskId */,
+            "mockLocus",
+            true /* isDismissible */,
+            mainExecutor,
+            metadataFlagListener
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 3fe78ef..445f74a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -124,7 +124,7 @@
         repo.addVisibleTasksListener(listener, executor)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
     }
 
@@ -148,7 +148,7 @@
         repo.addVisibleTasksListener(listener, executor)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
         // One call as adding listener notifies it
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0)
     }
@@ -162,8 +162,8 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
     }
 
     @Test
@@ -175,16 +175,16 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0)
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
 
         repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true)
         executor.flushAll()
 
         // Listener for secondary display is notified
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
         // No changes to listener for default display
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
@@ -198,7 +198,7 @@
 
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
 
         // Mark task 1 visible on secondary display
         repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true)
@@ -208,11 +208,11 @@
         // 1 - visible task added
         // 2 - visible task removed
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
 
         // Secondary display should have 1 call for visible task added
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
     }
 
     @Test
@@ -224,17 +224,17 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
         executor.flushAll()
 
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
 
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
     }
 
     @Test
@@ -397,8 +397,8 @@
     }
 
     class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
-        var hasVisibleTasksOnDefaultDisplay = false
-        var hasVisibleTasksOnSecondaryDisplay = false
+        var visibleTasksCountOnDefaultDisplay = 0
+        var visibleTasksCountOnSecondaryDisplay = 0
 
         var visibleChangesOnDefaultDisplay = 0
         var visibleChangesOnSecondaryDisplay = 0
@@ -409,14 +409,14 @@
         var stashedChangesOnDefaultDisplay = 0
         var stashedChangesOnSecondaryDisplay = 0
 
-        override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
             when (displayId) {
                 DEFAULT_DISPLAY -> {
-                    hasVisibleTasksOnDefaultDisplay = hasVisibleFreeformTasks
+                    visibleTasksCountOnDefaultDisplay = visibleTasksCount
                     visibleChangesOnDefaultDisplay++
                 }
                 SECOND_DISPLAY -> {
-                    hasVisibleTasksOnSecondaryDisplay = hasVisibleFreeformTasks
+                    visibleTasksCountOnSecondaryDisplay = visibleTasksCount
                     visibleChangesOnSecondaryDisplay++
                 }
                 else -> fail("Visible task listener received unexpected display id: $displayId")
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 193f16d..40e61dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -27,6 +27,8 @@
 
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -202,6 +204,8 @@
                 .setTaskDescriptionBuilder(taskDescriptionBuilder)
                 .setVisible(visible)
                 .build();
+        taskInfo.topActivityInfo = new ActivityInfo();
+        taskInfo.topActivityInfo.applicationInfo = new ApplicationInfo();
         taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor",
                 "DesktopModeWindowDecorationTests");
         taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 008ea3a..14b8d8d 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -341,6 +341,10 @@
     mCanvas->concat(matrix);
 }
 
+void SkiaCanvas::concat(const SkM44& matrix) {
+    mCanvas->concat(matrix);
+}
+
 void SkiaCanvas::rotate(float degrees) {
     mCanvas->rotate(degrees);
 }
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 4bf1790..5e3553b 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -86,6 +86,7 @@
     virtual void getMatrix(SkMatrix* outMatrix) const override;
     virtual void setMatrix(const SkMatrix& matrix) override;
     virtual void concat(const SkMatrix& matrix) override;
+    virtual void concat(const SkM44& matrix) override;
     virtual void rotate(float degrees) override;
     virtual void scale(float sx, float sy) override;
     virtual void skew(float sx, float sy) override;
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 9ec023b..20e3ad2 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -175,6 +175,7 @@
     virtual void setMatrix(const SkMatrix& matrix) = 0;
 
     virtual void concat(const SkMatrix& matrix) = 0;
+    virtual void concat(const SkM44& matrix) = 0;
     virtual void rotate(float degrees) = 0;
     virtual void scale(float sx, float sy) = 0;
     virtual void skew(float sx, float sy) = 0;
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index d572593..295f4dc 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -158,6 +158,13 @@
     get_canvas(canvasHandle)->concat(*matrix);
 }
 
+static void concat44(JNIEnv* env, jobject obj, jlong canvasHandle, jfloatArray arr) {
+    jfloat* matVals = env->GetFloatArrayElements(arr, 0);
+    const SkM44 matrix = SkM44::RowMajor(matVals);
+    get_canvas(canvasHandle)->concat(matrix);
+    env->ReleaseFloatArrayElements(arr, matVals, 0);
+}
+
 static void rotate(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jfloat degrees) {
     get_canvas(canvasHandle)->rotate(degrees);
 }
@@ -781,6 +788,7 @@
     {"nGetMatrix", "(JJ)V", (void*)CanvasJNI::getMatrix},
     {"nSetMatrix","(JJ)V", (void*) CanvasJNI::setMatrix},
     {"nConcat","(JJ)V", (void*) CanvasJNI::concat},
+    {"nConcat","(J[F)V", (void*) CanvasJNI::concat44},
     {"nRotate","(JF)V", (void*) CanvasJNI::rotate},
     {"nScale","(JFF)V", (void*) CanvasJNI::scale},
     {"nSkew","(JFF)V", (void*) CanvasJNI::skew},
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index d55d28d..b5f7caa 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -31,6 +31,8 @@
 #include <vk/GrVkExtensions.h>
 #include <vk/GrVkTypes.h>
 
+#include <sstream>
+
 #include "Properties.h"
 #include "RenderThread.h"
 #include "pipeline/skia/ShaderCache.h"
@@ -40,7 +42,8 @@
 namespace uirenderer {
 namespace renderthread {
 
-static std::array<std::string_view, 20> sEnableExtensions{
+// Not all of these are strictly required, but are all enabled if present.
+static std::array<std::string_view, 21> sEnableExtensions{
         VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
         VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
         VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -61,6 +64,7 @@
         VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
         VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
         VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
+        VK_EXT_DEVICE_FAULT_EXTENSION_NAME,
 };
 
 static bool shouldEnableExtension(const std::string_view& extension) {
@@ -303,6 +307,15 @@
     *tailPNext = ycbcrFeature;
     tailPNext = &ycbcrFeature->pNext;
 
+    if (grExtensions.hasExtension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME, 1)) {
+        VkPhysicalDeviceFaultFeaturesEXT* deviceFaultFeatures =
+                new VkPhysicalDeviceFaultFeaturesEXT;
+        deviceFaultFeatures->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT;
+        deviceFaultFeatures->pNext = nullptr;
+        *tailPNext = deviceFaultFeatures;
+        tailPNext = &deviceFaultFeatures->pNext;
+    }
+
     // query to get the physical device features
     mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features);
     // this looks like it would slow things down,
@@ -405,6 +418,79 @@
     });
 }
 
+namespace {
+void onVkDeviceFault(const std::string& contextLabel, const std::string& description,
+                     const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos,
+                     const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos,
+                     const std::vector<std::byte>& vendorBinaryData) {
+    // The final crash string should contain as much differentiating info as possible, up to 1024
+    // bytes. As this final message is constructed, the same information is also dumped to the logs
+    // but in a more verbose format. Building the crash string is unsightly, so the clearer logging
+    // statement is always placed first to give context.
+    ALOGE("VK_ERROR_DEVICE_LOST (%s context): %s", contextLabel.c_str(), description.c_str());
+    std::stringstream crashMsg;
+    crashMsg << "VK_ERROR_DEVICE_LOST (" << contextLabel;
+
+    if (!addressInfos.empty()) {
+        ALOGE("%zu VkDeviceFaultAddressInfoEXT:", addressInfos.size());
+        crashMsg << ", " << addressInfos.size() << " address info (";
+        for (VkDeviceFaultAddressInfoEXT addressInfo : addressInfos) {
+            ALOGE(" addressType:       %d", (int)addressInfo.addressType);
+            ALOGE("  reportedAddress:  %" PRIu64, addressInfo.reportedAddress);
+            ALOGE("  addressPrecision: %" PRIu64, addressInfo.addressPrecision);
+            crashMsg << addressInfo.addressType << ":"
+                     << addressInfo.reportedAddress << ":"
+                     << addressInfo.addressPrecision << ", ";
+        }
+        crashMsg.seekp(-2, crashMsg.cur);  // Move back to overwrite trailing ", "
+        crashMsg << ")";
+    }
+
+    if (!vendorInfos.empty()) {
+        ALOGE("%zu VkDeviceFaultVendorInfoEXT:", vendorInfos.size());
+        crashMsg << ", " << vendorInfos.size() << " vendor info (";
+        for (VkDeviceFaultVendorInfoEXT vendorInfo : vendorInfos) {
+            ALOGE(" description:      %s", vendorInfo.description);
+            ALOGE("  vendorFaultCode: %" PRIu64, vendorInfo.vendorFaultCode);
+            ALOGE("  vendorFaultData: %" PRIu64, vendorInfo.vendorFaultData);
+            // Omit descriptions for individual vendor info structs in the crash string, as the
+            // fault code and fault data fields should be enough for clustering, and the verbosity
+            // isn't worth it. Additionally, vendors may just set the general description field of
+            // the overall fault to the description of the first element in this list, and that
+            // overall description will be placed at the end of the crash string.
+            crashMsg << vendorInfo.vendorFaultCode << ":"
+                     << vendorInfo.vendorFaultData << ", ";
+        }
+        crashMsg.seekp(-2, crashMsg.cur);  // Move back to overwrite trailing ", "
+        crashMsg << ")";
+    }
+
+    if (!vendorBinaryData.empty()) {
+        // TODO: b/322830575 - Log in base64, or dump directly to a file that gets put in bugreports
+        ALOGE("%zu bytes of vendor-specific binary data (please notify Android's Core Graphics"
+              " Stack team if you observe this message).",
+              vendorBinaryData.size());
+        crashMsg << ", " << vendorBinaryData.size() << " bytes binary";
+    }
+
+    crashMsg << "): " << description;
+    LOG_ALWAYS_FATAL("%s", crashMsg.str().c_str());
+}
+
+void deviceLostProcRenderThread(void* callbackContext, const std::string& description,
+                                const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos,
+                                const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos,
+                                const std::vector<std::byte>& vendorBinaryData) {
+    onVkDeviceFault("RenderThread", description, addressInfos, vendorInfos, vendorBinaryData);
+}
+void deviceLostProcUploadThread(void* callbackContext, const std::string& description,
+                                const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos,
+                                const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos,
+                                const std::vector<std::byte>& vendorBinaryData) {
+    onVkDeviceFault("UploadThread", description, addressInfos, vendorInfos, vendorBinaryData);
+}
+}  // anonymous namespace
+
 static void onGrContextReleased(void* context) {
     VulkanManager* manager = (VulkanManager*)context;
     manager->decStrong((void*)onGrContextReleased);
@@ -430,6 +516,10 @@
     backendContext.fVkExtensions = &mExtensions;
     backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2;
     backendContext.fGetProc = std::move(getProc);
+    backendContext.fDeviceLostContext = nullptr;
+    backendContext.fDeviceLostProc = (contextType == ContextType::kRenderThread)
+                                             ? deviceLostProcRenderThread
+                                             : deviceLostProcUploadThread;
 
     LOG_ALWAYS_FATAL_IF(options.fContextDeleteProc != nullptr, "Conflicting fContextDeleteProcs!");
     this->incStrong((void*)onGrContextReleased);
diff --git a/location/api/current.txt b/location/api/current.txt
index c55676b..c7954fe 100644
--- a/location/api/current.txt
+++ b/location/api/current.txt
@@ -682,6 +682,7 @@
   public final class AltitudeConverter {
     ctor public AltitudeConverter();
     method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+    method @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull android.location.Location);
   }
 
 }
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 6f88912..461dafb 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -16,12 +16,14 @@
 
 package android.location.altitude;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.WorkerThread;
 import android.content.Context;
 import android.frameworks.location.altitude.GetGeoidHeightRequest;
 import android.frameworks.location.altitude.GetGeoidHeightResponse;
 import android.location.Location;
+import android.location.flags.Flags;
 
 import com.android.internal.location.altitude.GeoidMap;
 import com.android.internal.location.altitude.S2CellIdUtils;
@@ -213,12 +215,12 @@
     }
 
     /**
-     * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that data will not be
-     * loaded from raw assets. Returns true if a Mean Sea Level altitude is added to the
-     * {@code location}; otherwise, returns false and leaves the {@code location} unchanged.
-     *
-     * @hide
+     * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that this method can be
+     * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean
+     * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the
+     * {@code location} unchanged.
      */
+    @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL)
     public boolean addMslAltitudeToLocation(@NonNull Location location) {
         validate(location);
         MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams();
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/location.aconfig
similarity index 77%
rename from location/java/android/location/flags/gnss.aconfig
rename to location/java/android/location/flags/location.aconfig
index 8c7c871..a96fe47 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -1,6 +1,20 @@
 package: "android.location.flags"
 
 flag {
+    name: "fix_service_watcher"
+    namespace: "location"
+    description: "Enable null explicit services in ServiceWatcher"
+    bug: "311210517"
+}
+
+flag {
+    name: "geoid_heights_via_altitude_hal"
+    namespace: "location"
+    description: "Flag for making geoid heights available via the Altitude HAL"
+    bug: "304375846"
+}
+
+flag {
     name: "gnss_api_navic_l1"
     namespace: "location"
     description: "Flag for GNSS API for NavIC L1"
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4918289..69708ec 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -23,6 +23,7 @@
 import static android.media.audio.Flags.automaticBtDeviceType;
 import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
 import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
 import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.Manifest;
@@ -80,6 +81,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
 import android.view.KeyEvent;
@@ -100,6 +102,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
@@ -7838,6 +7841,51 @@
     }
 
     /**
+     * Returns a Set of unique Integers corresponding to audio device type identifiers that can
+     * <i>potentially</i> be connected to the system and meeting the criteria specified in the
+     * <code>direction</code> parameter.
+     * Note that this set contains {@link AudioDeviceInfo} device type identifiers for both devices
+     * currently available <i>and</i> those that can be available if the user connects an audio
+     * peripheral. Examples include TYPE_WIRED_HEADSET if the Android device supports an analog
+     * headset jack or TYPE_USB_DEVICE if the Android device supports a USB host-mode port.
+     * These are generally a superset of device type identifiers associated with the
+     * AudioDeviceInfo objects returned from AudioManager.getDevices().
+     * @param direction The constant specifying whether input or output devices are queried.
+     * @see #GET_DEVICES_OUTPUTS
+     * @see #GET_DEVICES_INPUTS
+     * @return A (possibly zero-length) Set of Integer objects corresponding to the audio
+     * device types of devices supported by the implementation.
+     * @throws IllegalArgumentException If an invalid direction constant is specified.
+     */
+    @FlaggedApi(FLAG_SUPPORTED_DEVICE_TYPES_API)
+    public @NonNull Set<Integer>
+            getSupportedDeviceTypes(int direction) {
+        if (direction != GET_DEVICES_OUTPUTS && direction != GET_DEVICES_INPUTS) {
+            throw new IllegalArgumentException("AudioManager.getSupportedDeviceTypes("
+                    + Integer.toHexString(direction) + ") - Invalid.");
+        }
+
+        IntArray internalDeviceTypes = new IntArray();
+        int status = AudioSystem.getSupportedDeviceTypes(direction, internalDeviceTypes);
+        if (status != AudioManager.SUCCESS) {
+            Log.e(TAG, "AudioManager.getSupportedDeviceTypes(" + direction + ") failed. status:"
+                    + status);
+        }
+
+        // convert to external (AudioDeviceInfo.getType()) device IDs
+        HashSet<Integer> externalDeviceTypes = new HashSet<Integer>();
+        for (int index = 0; index < internalDeviceTypes.size(); index++) {
+            // Set will eliminate any duplicates which AudioSystem.getSupportedDeviceTypes()
+            // returns
+            externalDeviceTypes.add(
+                    AudioDeviceInfo.convertInternalDeviceToDeviceType(
+                        internalDeviceTypes.get(index)));
+        }
+
+        return externalDeviceTypes;
+    }
+
+     /**
      * Returns an array of {@link AudioDeviceInfo} objects corresponding to the audio devices
      * currently connected to the system and meeting the criteria specified in the
      * <code>flags</code> parameter.
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 0f6cbff..f73be35f 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -36,6 +36,7 @@
 import android.os.Parcel;
 import android.os.Vibrator;
 import android.telephony.TelephonyManager;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
 
@@ -1947,6 +1948,8 @@
     /** @hide */
     public static native int listAudioPorts(ArrayList<AudioPort> ports, int[] generation);
     /** @hide */
+    public static native int getSupportedDeviceTypes(int flags, IntArray internalDeviceTypes);
+    /** @hide */
     public static native int createAudioPatch(AudioPatch[] patch,
                                             AudioPortConfig[] sources, AudioPortConfig[] sinks);
     /** @hide */
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 72e6db8..5ed8d40 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -86,7 +86,10 @@
      */
     public static final int ERROR_CODE_IO_NO_PERMISSION = 8;
 
-    /** */
+    /**
+     * Caused by failing to load data via cleartext HTTP, when the app's network security
+     * configuration does not permit it.
+     */
     public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9;
 
     /** Caused by reading data out of the data bounds. */
@@ -146,6 +149,9 @@
     @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
     public @interface ErrorCode {}
 
+    /** Special value for unknown {@linkplain #getTimeSinceCreatedMillis() time since creation}. */
+    public static final int TIME_SINCE_CREATED_UNKNOWN = -1;
+
     private final @ErrorCode int mErrorCode;
     @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
     private final long mTimeSinceCreatedMillis;
@@ -174,16 +180,16 @@
     }
 
     /**
-     * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if
-     * unknown.
+     * Gets the elapsed time since creating of the editing session, in milliseconds, or {@link
+     * #TIME_SINCE_CREATED_UNKNOWN} if unknown.
      *
-     * @return The elapsed time since creating the editing session, in milliseconds, or -1 if
-     *     unknown.
+     * @return The elapsed time since creating the editing session, in milliseconds, or {@link
+     *     #TIME_SINCE_CREATED_UNKNOWN} if unknown.
      * @see LogSessionId
      * @see EditingSession
      */
     @Override
-    @IntRange(from = -1)
+    @IntRange(from = TIME_SINCE_CREATED_UNKNOWN)
     public long getTimeSinceCreatedMillis() {
         return mTimeSinceCreatedMillis;
     }
@@ -283,7 +289,7 @@
         public Builder(@FinalState int finalState) {
             mFinalState = finalState;
             mErrorCode = ERROR_CODE_NONE;
-            mTimeSinceCreatedMillis = -1;
+            mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
             mMetricsBundle = new Bundle();
         }
 
@@ -291,11 +297,11 @@
          * Sets the elapsed time since creating the editing session, in milliseconds.
          *
          * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in
-         *     milliseconds, or -1 if the value is unknown.
+         *     milliseconds, or {@link #TIME_SINCE_CREATED_UNKNOWN} if unknown.
          * @see #getTimeSinceCreatedMillis()
          */
         public @NonNull Builder setTimeSinceCreatedMillis(
-                @IntRange(from = -1) long timeSinceCreatedMillis) {
+                @IntRange(from = TIME_SINCE_CREATED_UNKNOWN) long timeSinceCreatedMillis) {
             mTimeSinceCreatedMillis = timeSinceCreatedMillis;
             return this;
         }
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 8978277..b644621 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -69,4 +69,6 @@
     // For ad response
     void onAdResponse(in AdResponse response, int seq);
     void onAdBufferConsumed(in AdBuffer buffer, int seq);
+
+    void onTvInputSessionData(in String type, in Bundle data, int seq);
 }
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 2f6575e..84c197d 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -152,4 +152,6 @@
 
     // For freezing video playback
     void setVideoFrozen(in IBinder sessionToken, boolean isFrozen, int userId);
+
+    void notifyTvAdSessionData(in IBinder sessionToken, in String type, in Bundle data, int userId);
 }
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index a93f18d..7b20c39 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -86,4 +86,6 @@
 
     // For freezing video
     void setVideoFrozen(boolean isFrozen);
+
+    void notifyTvAdSessionData(in String type, in Bundle data);
 }
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index 8e2702a..76e079f 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -68,4 +68,6 @@
 
     // For messages sent from the TV input
     void onTvMessage(int type, in Bundle data);
+
+    void onTvInputSessionData(in String type, in Bundle data);
 }
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 921104d..999e2cf 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -82,6 +82,7 @@
     private static final int DO_STOP_PLAYBACK = 33;
     private static final int DO_START_PLAYBACK = 34;
     private static final int DO_SET_VIDEO_FROZEN = 35;
+    private static final int DO_NOTIFY_AD_SESSION_DATA = 36;
 
     private final boolean mIsRecordingSession;
     private final HandlerCaller mCaller;
@@ -287,6 +288,7 @@
             case DO_NOTIFY_TV_MESSAGE: {
                 SomeArgs args = (SomeArgs) msg.obj;
                 mTvInputSessionImpl.onTvMessageReceived((Integer) args.arg1, (Bundle) args.arg2);
+                args.recycle();
                 break;
             }
             case DO_STOP_PLAYBACK: {
@@ -301,6 +303,12 @@
                 mTvInputSessionImpl.setVideoFrozen((Boolean) msg.obj);
                 break;
             }
+            case DO_NOTIFY_AD_SESSION_DATA: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mTvInputSessionImpl.notifyTvAdSessionData((String) args.arg1, (Bundle) args.arg2);
+                args.recycle();
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -488,6 +496,12 @@
     }
 
     @Override
+    public void notifyTvAdSessionData(String type, Bundle data) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageOO(DO_NOTIFY_AD_SESSION_DATA, type, data));
+    }
+
+    @Override
     public void setVideoFrozen(boolean isFrozen) {
         mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VIDEO_FROZEN, isFrozen));
     }
diff --git a/media/java/android/media/tv/SignalingDataInfo.aidl b/media/java/android/media/tv/SignalingDataInfo.aidl
new file mode 100644
index 0000000..7108f36
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataInfo.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataInfo;
diff --git a/media/java/android/media/tv/SignalingDataInfo.java b/media/java/android/media/tv/SignalingDataInfo.java
new file mode 100644
index 0000000..b29ea5c
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataInfo.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+/** @hide */
+public class SignalingDataInfo implements Parcelable {
+    public static final @NonNull Parcelable.Creator<SignalingDataInfo> CREATOR =
+            new Parcelable.Creator<SignalingDataInfo>() {
+                @Override
+                public SignalingDataInfo[] newArray(int size) {
+                    return new SignalingDataInfo[size];
+                }
+
+                @Override
+                public SignalingDataInfo createFromParcel(@NonNull android.os.Parcel in) {
+                    return new SignalingDataInfo(in);
+                }
+            };
+
+    private int mTableId;
+    private @NonNull String mTable;
+    private int mMetadataType;
+    private int mVersion;
+    private int mGroup;
+    private @NonNull String mEncoding;
+
+    public SignalingDataInfo(
+            int tableId,
+            @NonNull String table,
+            int metadataType,
+            int version,
+            int group,
+            @NonNull String encoding) {
+        this.mTableId = tableId;
+        this.mTable = table;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTable);
+        this.mMetadataType = metadataType;
+        this.mVersion = version;
+        this.mGroup = group;
+        this.mEncoding = encoding;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mEncoding);
+    }
+
+    public int getTableId() {
+        return mTableId;
+    }
+
+    public @NonNull String getTable() {
+        return mTable;
+    }
+
+    public int getMetadataType() {
+        return mMetadataType;
+    }
+
+    public int getVersion() {
+        return mVersion;
+    }
+
+    public int getGroup() {
+        return mGroup;
+    }
+
+    public @NonNull String getEncoding() {
+        return mEncoding;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        dest.writeInt(mTableId);
+        dest.writeString(mTable);
+        dest.writeInt(mMetadataType);
+        dest.writeInt(mVersion);
+        dest.writeInt(mGroup);
+        dest.writeString(mEncoding);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    SignalingDataInfo(@NonNull android.os.Parcel in) {
+        int tableId = in.readInt();
+        String table = in.readString();
+        int metadataType = in.readInt();
+        int version = in.readInt();
+        int group = in.readInt();
+        String encoding = in.readString();
+
+        this.mTableId = tableId;
+        this.mTable = table;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTable);
+        this.mMetadataType = metadataType;
+        this.mVersion = version;
+        this.mGroup = group;
+        this.mEncoding = encoding;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mEncoding);
+    }
+}
diff --git a/media/java/android/media/tv/SignalingDataResponse.aidl b/media/java/android/media/tv/SignalingDataResponse.aidl
new file mode 100644
index 0000000..a548e4e
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataResponse.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataResponse;
diff --git a/media/java/android/media/tv/SignalingDataResponse.java b/media/java/android/media/tv/SignalingDataResponse.java
new file mode 100644
index 0000000..3e4c790
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataResponse.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/** @hide */
+public class SignalingDataResponse extends BroadcastInfoResponse implements Parcelable {
+    public static final @NonNull Parcelable.Creator<SignalingDataResponse> CREATOR =
+            new Parcelable.Creator<SignalingDataResponse>() {
+                @Override
+                public SignalingDataResponse[] newArray(int size) {
+                    return new SignalingDataResponse[size];
+                }
+
+                @Override
+                public SignalingDataResponse createFromParcel(@NonNull android.os.Parcel in) {
+                    return new SignalingDataResponse(in);
+                }
+            };
+    private static final @TvInputManager.BroadcastInfoType int RESPONSE_TYPE =
+            TvInputManager.BROADCAST_INFO_TYPE_SIGNALING_DATA;
+    private final @NonNull int[] mTableIds;
+    private final int mMetadataTypes;
+    private final @NonNull List<SignalingDataInfo> mSignalingDataInfoList;
+
+    public SignalingDataResponse(
+            int requestId,
+            int sequence,
+            @ResponseResult int responseResult,
+            @NonNull int[] tableIds,
+            int metadataTypes,
+            @NonNull List<SignalingDataInfo> signalingDataInfoList) {
+        super(RESPONSE_TYPE, requestId, sequence, responseResult);
+        this.mTableIds = tableIds;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTableIds);
+        this.mMetadataTypes = metadataTypes;
+        this.mSignalingDataInfoList = signalingDataInfoList;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSignalingDataInfoList);
+    }
+
+    public @NonNull int[] getTableIds() {
+        return mTableIds;
+    }
+
+    public int getMetadataTypes() {
+        return mMetadataTypes;
+    }
+
+    public @NonNull List<SignalingDataInfo> getSignalingDataInfoList() {
+        return mSignalingDataInfoList;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeIntArray(mTableIds);
+        dest.writeInt(mMetadataTypes);
+        dest.writeParcelableList(mSignalingDataInfoList, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    SignalingDataResponse(@NonNull android.os.Parcel in) {
+        super(RESPONSE_TYPE, in);
+
+        int[] tableIds = in.createIntArray();
+        int metadataTypes = in.readInt();
+        List<SignalingDataInfo> signalingDataInfoList = new java.util.ArrayList<>();
+        in.readParcelableList(signalingDataInfoList, SignalingDataInfo.class.getClassLoader());
+
+        this.mTableIds = tableIds;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTableIds);
+        this.mMetadataTypes = metadataTypes;
+        this.mSignalingDataInfoList = signalingDataInfoList;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSignalingDataInfoList);
+    }
+}
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index be1b675..8720bfe 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
@@ -535,6 +536,220 @@
      */
     public static final int SIGNAL_STRENGTH_STRONG = 3;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "SESSION_DATA_TYPE_", value = {
+            SESSION_DATA_TYPE_TUNED,
+            SESSION_DATA_TYPE_TRACK_SELECTED,
+            SESSION_DATA_TYPE_TRACKS_CHANGED,
+            SESSION_DATA_TYPE_VIDEO_AVAILABLE,
+            SESSION_DATA_TYPE_VIDEO_UNAVAILABLE,
+            SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE,
+            SESSION_DATA_TYPE_AD_RESPONSE,
+            SESSION_DATA_TYPE_AD_BUFFER_CONSUMED,
+            SESSION_DATA_TYPE_TV_MESSAGE})
+    public @interface SessionDataType {}
+
+    /**
+     * Informs the application that the session has been tuned to the given channel.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_CHANNEL_URI
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_TUNED = "tuned";
+
+    /**
+     * Sends the type and ID of a selected track. This is used to inform the application that a
+     * specific track is selected.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_TRACK_TYPE
+     * @see SESSION_DATA_KEY_TRACK_ID
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_TRACK_SELECTED = "track_selected";
+
+    /**
+     * Sends the list of all audio/video/subtitle tracks.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_TRACKS
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_TRACKS_CHANGED = "tracks_changed";
+
+    /**
+     * Informs the application that the video is now available for watching.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_VIDEO_AVAILABLE = "video_available";
+
+    /**
+     * Informs the application that the video became unavailable for some reason.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_VIDEO_UNAVAILABLE = "video_unavailable";
+
+    /**
+     * Notifies response for broadcast info.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE =
+            "broadcast_info_response";
+
+    /**
+     * Notifies response for advertisement.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_AD_RESPONSE
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_AD_RESPONSE = "ad_response";
+
+    /**
+     * Notifies the advertisement buffer is consumed.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_AD_BUFFER
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_AD_BUFFER_CONSUMED = "ad_buffer_consumed";
+
+    /**
+     * Sends the TV message.
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @see TvInputService.Session#notifyTvMessage(int, Bundle)
+     * @see SESSION_DATA_KEY_TV_MESSAGE_TYPE
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_TV_MESSAGE = "tv_message";
+
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "SESSION_DATA_KEY_", value = {
+            SESSION_DATA_KEY_CHANNEL_URI,
+            SESSION_DATA_KEY_TRACK_TYPE,
+            SESSION_DATA_KEY_TRACK_ID,
+            SESSION_DATA_KEY_TRACKS,
+            SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON,
+            SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE,
+            SESSION_DATA_KEY_AD_RESPONSE,
+            SESSION_DATA_KEY_AD_BUFFER,
+            SESSION_DATA_KEY_TV_MESSAGE_TYPE})
+    public @interface SessionDataKey {}
+
+    /**
+     * The URI of a channel.
+     *
+     * <p> Type: android.net.Uri
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_CHANNEL_URI = "channel_uri";
+
+    /**
+     * The type of the track.
+     *
+     * <p>One of {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO},
+     * {@link TvTrackInfo#TYPE_SUBTITLE}.
+     *
+     * <p> Type: Integer
+     *
+     * @see TvTrackInfo#getType()
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_TRACK_TYPE = "track_type";
+
+    /**
+     * The ID of the track.
+     *
+     * <p> Type: String
+     *
+     * @see TvTrackInfo#getId()
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_TRACK_ID = "track_id";
+
+    /**
+     * A list which includes track information.
+     *
+     * <p> Type: {@code java.util.List<android.media.tv.TvTrackInfo> }
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_TRACKS = "tracks";
+
+    /**
+     * The reason why the video became unavailable.
+     * <p>The value can be {@link VIDEO_UNAVAILABLE_REASON_BUFFERING},
+     * {@link VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}, etc.
+     *
+     * <p> Type: Integer
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON =
+            "video_unavailable_reason";
+
+    /**
+     * An object of {@link BroadcastInfoResponse}.
+     *
+     * <p> Type: android.media.tv.BroadcastInfoResponse
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE = "broadcast_info_response";
+
+    /**
+     * An object of {@link AdResponse}.
+     *
+     * <p> Type: android.media.tv.AdResponse
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_AD_RESPONSE = "ad_response";
+
+    /**
+     * An object of {@link AdBuffer}.
+     *
+     * <p> Type: android.media.tv.AdBuffer
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+
+    /**
+     * The type of TV message.
+     * <p>It can be one of {@link TV_MESSAGE_TYPE_WATERMARK},
+     * {@link TV_MESSAGE_TYPE_CLOSED_CAPTION}, {@link TV_MESSAGE_TYPE_OTHER}
+     *
+     * <p> Type: Integer
+     *
+     * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_TV_MESSAGE_TYPE = "tv_message_type";
+
+
     /**
      * An unknown state of the client pid gets from the TvInputManager. Client gets this value when
      * query through {@link getClientPid(String sessionId)} fails.
@@ -1271,6 +1486,17 @@
                 });
             }
         }
+
+        void postTvInputSessionData(String type, Bundle data) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mSession.getAdSession() != null) {
+                        mSession.getAdSession().notifyTvInputSessionData(type, data);
+                    }
+                }
+            });
+        }
     }
 
     /**
@@ -1820,6 +2046,18 @@
                     record.postAdBufferConsumed(buffer);
                 }
             }
+
+            @Override
+            public void onTvInputSessionData(String type, Bundle data, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTvInputSessionData(type, data);
+                }
+            }
         };
         ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() {
             @Override
@@ -3844,6 +4082,21 @@
             }
         }
 
+        /**
+         * Notifies data from session of linked TvAdService.
+         */
+        public void notifyTvAdSessionData(String type, Bundle data) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTvAdSessionData(mToken, type, data, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         private final class InputEventHandler extends Handler {
             public static final int MSG_SEND_INPUT_EVENT = 1;
             public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 6301a27..a022b1c 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -34,6 +34,7 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.media.AudioPresentation;
 import android.media.PlaybackParams;
+import android.media.tv.ad.TvAdManager;
 import android.media.tv.interactive.TvInteractiveAppService;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -1259,6 +1260,37 @@
         }
 
         /**
+         * Notifies data related to this session to corresponding linked
+         * {@link android.media.tv.ad.TvAdService} object via TvAdView.
+         *
+         * <p>Methods like {@link #notifyBroadcastInfoResponse(BroadcastInfoResponse)} sends the
+         * related data to linked {@link android.media.tv.interactive.TvInteractiveAppService}, but
+         * don't work for {@link android.media.tv.ad.TvAdService}. The method is used specifically
+         * for {@link android.media.tv.ad.TvAdService} use cases.
+         *
+         * @param type data type
+         * @param data the related data values
+         * @hide
+         */
+        public void notifyTvInputSessionData(
+                @NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTvInputSessionData");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTvInputSessionData(type, data);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTvInputSessionData", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
          * is relative to the overlay view that sits on top of this surface.
          *
@@ -1401,6 +1433,20 @@
         public void onAdBufferReady(@NonNull AdBuffer buffer) {
         }
 
+
+        /**
+         * Called when data from the linked {@link android.media.tv.ad.TvAdService} is received.
+         *
+         * @param type the type of the data
+         * @param data a bundle contains the data received
+         * @see android.media.tv.ad.TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+         * @see android.media.tv.ad.TvAdView#setTvView(TvView)
+         * @hide
+         */
+        public void onTvAdSessionData(
+                @NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
+        }
+
         /**
          * Tunes to a given channel.
          *
@@ -2166,6 +2212,10 @@
             onAdBufferReady(buffer);
         }
 
+        void notifyTvAdSessionData(String type, Bundle data) {
+            onTvAdSessionData(type, data);
+        }
+
         void onTvMessageReceived(int type, Bundle data) {
             onTvMessage(type, data);
         }
diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl
index 34d96b3..49046d0 100644
--- a/media/java/android/media/tv/ad/ITvAdClient.aidl
+++ b/media/java/android/media/tv/ad/ITvAdClient.aidl
@@ -16,6 +16,7 @@
 
 package android.media.tv.ad;
 
+import android.os.Bundle;
 import android.view.InputChannel;
 
 /**
@@ -27,4 +28,11 @@
     void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq);
     void onSessionReleased(int seq);
     void onLayoutSurface(int left, int top, int right, int bottom, int seq);
+    void onRequestCurrentVideoBounds(int seq);
+    void onRequestCurrentChannelUri(int seq);
+    void onRequestTrackInfoList(int seq);
+    void onRequestCurrentTvInputId(int seq);
+    void onRequestSigning(
+            in String id, in String algorithm, in String alias, in byte[] data, int seq);
+    void onTvAdSessionData(in String type, in Bundle data, int seq);
 }
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
index 9620065..5c5f095 100644
--- a/media/java/android/media/tv/ad/ITvAdManager.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -31,6 +31,7 @@
  */
 interface ITvAdManager {
     List<TvAdServiceInfo> getTvAdServiceList(int userId);
+    void sendAppLinkCommand(String serviceId, in Bundle command, int userId);
     void createSession(
             in ITvAdClient client, in String serviceId, in String type, int seq, int userId);
     void releaseSession(in IBinder sessionToken, int userId);
@@ -58,4 +59,7 @@
             int userId);
     void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId);
     void removeMediaView(in IBinder sessionToken, int userId);
+
+    void notifyTvInputSessionData(
+            in IBinder sessionToken, in String type, in Bundle data, int userId);
 }
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
index 69afb17..eba0bc8 100644
--- a/media/java/android/media/tv/ad/ITvAdSession.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -46,4 +46,6 @@
     void createMediaView(in IBinder windowToken, in Rect frame);
     void relayoutMediaView(in Rect frame);
     void removeMediaView();
+
+    void notifyTvInputSessionData(in String type, in Bundle data);
 }
diff --git a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
index f21ef19..e4e5cbb 100644
--- a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
@@ -17,6 +17,7 @@
 package android.media.tv.ad;
 
 import android.media.tv.ad.ITvAdSession;
+import android.os.Bundle;
 
 /**
  * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is
@@ -26,4 +27,10 @@
 oneway interface ITvAdSessionCallback {
     void onSessionCreated(in ITvAdSession session);
     void onLayoutSurface(int left, int top, int right, int bottom);
+    void onRequestCurrentVideoBounds();
+    void onRequestCurrentChannelUri();
+    void onRequestTrackInfoList();
+    void onRequestCurrentTvInputId();
+    void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
+    void onTvAdSessionData(in String type, in Bundle data);
 }
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
index 251351d..e10a20e 100644
--- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
+++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
@@ -65,6 +65,7 @@
     private static final int DO_SEND_SIGNING_RESULT = 14;
     private static final int DO_NOTIFY_ERROR = 15;
     private static final int DO_NOTIFY_TV_MESSAGE = 16;
+    private static final int DO_NOTIFY_INPUT_SESSION_DATA = 17;
 
     private final HandlerCaller mCaller;
     private TvAdService.Session mSessionImpl;
@@ -180,6 +181,12 @@
                 args.recycle();
                 break;
             }
+            case DO_NOTIFY_INPUT_SESSION_DATA: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyTvInputSessionData((String) args.arg1, (Bundle) args.arg2);
+                args.recycle();
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -280,6 +287,12 @@
         mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW));
     }
 
+    @Override
+    public void notifyTvInputSessionData(String type, Bundle data) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageOO(DO_NOTIFY_INPUT_SESSION_DATA, type, data));
+    }
+
     private final class TvAdEventReceiver extends InputEventReceiver {
         TvAdEventReceiver(InputChannel inputChannel, Looper looper) {
             super(inputChannel, looper);
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 4dce72f..02c0d75 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -16,12 +16,15 @@
 
 package android.media.tv.ad;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.StringDef;
 import android.annotation.SystemService;
 import android.content.Context;
 import android.graphics.Rect;
+import android.media.tv.AdBuffer;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvTrackInfo;
 import android.media.tv.flags.Flags;
@@ -43,7 +46,10 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -57,6 +63,198 @@
     // TODO: implement more methods and unhide APIs.
     private static final String TAG = "TvAdManager";
 
+    /**
+     * Key for package name in app link.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
+
+    /**
+     * Key for class name in app link.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
+
+    /**
+     * Key for command type in app link command.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
+
+    /**
+     * Key for service ID in app link command.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+
+    /**
+     * Key for back URI in app link command.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String APP_LINK_KEY_BACK_URI = "back_uri";
+
+    /**
+     * Broadcast intent action to send app command to TV app.
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @hide
+     */
+    public static final String ACTION_APP_LINK_COMMAND =
+            "android.media.tv.ad.action.APP_LINK_COMMAND";
+
+    /**
+     * Intent key for TV input ID. It's used to send app command to TV app.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @see #ACTION_APP_LINK_COMMAND
+     * @hide
+     */
+    public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
+
+    /**
+     * Intent key for TV AD service ID. It's used to send app command to TV app.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @see #ACTION_APP_LINK_COMMAND
+     * @see TvAdServiceInfo#getId()
+     * @hide
+     */
+    public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
+
+    /**
+     * Intent key for TV channel URI. It's used to send app command to TV app.
+     * <p>Type: android.net.Uri
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @see #ACTION_APP_LINK_COMMAND
+     * @hide
+     */
+    public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
+
+    /**
+     * Intent key for command type. It's used to send app command to TV app. The value of this key
+     * could vary according to TV apps.
+     * <p>Type: String
+     *
+     * @see #sendAppLinkCommand(String, Bundle)
+     * @see #ACTION_APP_LINK_COMMAND
+     * @hide
+     */
+    public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "SESSION_DATA_TYPE_", value = {
+            SESSION_DATA_TYPE_AD_REQUEST,
+            SESSION_DATA_TYPE_AD_BUFFER_READY,
+            SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST,
+            SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST})
+    public @interface SessionDataType {}
+
+    /**
+     * Sends an advertisement request to be processed by the related TV input.
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_AD_REQUEST
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
+
+    /**
+     * Notifies the advertisement buffer is ready.
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_AD_BUFFER
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_AD_BUFFER_READY = "ad_buffer_ready";
+
+    /**
+     * Sends request for broadcast info.
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_BROADCAST_INFO_RESQUEST
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+
+    /**
+     * Removes request for broadcast info.
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @see SESSION_DATA_KEY_BROADCAST_INFO_REQUEST_ID
+     * @hide
+     */
+    public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST =
+            "remove_broadcast_info_request";
+
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "SESSION_DATA_KEY_", value = {
+            SESSION_DATA_KEY_AD_REQUEST,
+            SESSION_DATA_KEY_AD_BUFFER,
+            SESSION_DATA_KEY_BROADCAST_INFO_REQUEST,
+            SESSION_DATA_KEY_REQUEST_ID})
+    public @interface SessionDataKey {}
+
+    /**
+     * An object of {@link android.media.tv.AdRequest}.
+     *
+     * <p> Type: android.media.tv.AdRequest
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request";
+
+    /**
+     * An object of {@link AdBuffer}.
+     *
+     * <p> Type: android.media.tv.AdBuffer
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+
+    /**
+     * An object of {@link android.media.tv.BroadcastInfoRequest}.
+     *
+     * <p> Type: android.media.tv.BroadcastInfoRequest
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+
+    /**
+     * The ID of {@link android.media.tv.BroadcastInfoRequest}.
+     *
+     * <p> Type: Integer
+     *
+     * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+     * @hide
+     */
+    public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
+
     private final ITvAdManager mService;
     private final int mUserId;
 
@@ -125,6 +323,79 @@
                 }
             }
 
+            @Override
+            public void onRequestCurrentVideoBounds(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestCurrentVideoBounds();
+                }
+            }
+
+            @Override
+            public void onRequestCurrentChannelUri(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestCurrentChannelUri();
+                }
+            }
+
+            @Override
+            public void onRequestTrackInfoList(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestTrackInfoList();
+                }
+            }
+
+            @Override
+            public void onRequestCurrentTvInputId(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestCurrentTvInputId();
+                }
+            }
+
+            @Override
+            public void onRequestSigning(
+                    String id, String algorithm, String alias, byte[] data, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestSigning(id, algorithm, alias, data);
+                }
+            }
+
+            @Override
+            public void onTvAdSessionData(String type, Bundle data, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTvAdSessionData(type, data);
+                }
+            }
+
         };
 
         ITvAdManagerCallback managerCallback =
@@ -220,6 +491,59 @@
     }
 
     /**
+     * Sends app link command.
+     *
+     * @param serviceId The ID of TV AD service which the command to be sent to. The ID can be found
+     *                  in {@link TvAdServiceInfo#getId()}.
+     * @param command The command to be sent.
+     * @hide
+     */
+    public void sendAppLinkCommand(@NonNull String serviceId, @NonNull Bundle command) {
+        try {
+            mService.sendAppLinkCommand(serviceId, command, mUserId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers a {@link TvAdServiceCallback}.
+     *
+     * @param callback A callback used to monitor status of the TV AD services.
+     * @param executor A {@link Executor} that the status change will be delivered to.
+     * @hide
+     */
+    public void registerCallback(
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull TvAdServiceCallback callback) {
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(executor);
+        synchronized (mLock) {
+            mCallbackRecords.add(new TvAdServiceCallbackRecord(callback, executor));
+        }
+    }
+
+    /**
+     * Unregisters the existing {@link TvAdServiceCallback}.
+     *
+     * @param callback The existing callback to remove.
+     * @hide
+     */
+    public void unregisterCallback(@NonNull final TvAdServiceCallback callback) {
+        Preconditions.checkNotNull(callback);
+        synchronized (mLock) {
+            for (Iterator<TvAdServiceCallbackRecord> it = mCallbackRecords.iterator();
+                    it.hasNext(); ) {
+                TvAdServiceCallbackRecord record = it.next();
+                if (record.getCallback() == callback) {
+                    it.remove();
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
      * The Session provides the per-session functionality of AD service.
      * @hide
      */
@@ -533,6 +857,88 @@
             }
         }
 
+        /**
+         * Notifies data from session of linked TvInputService.
+         */
+        public void notifyTvInputSessionData(String type, Bundle data) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTvInputSessionData(mToken, type, data, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Dispatches an input event to this session.
+         *
+         * @param event An {@link InputEvent} to dispatch. Cannot be {@code null}.
+         * @param token A token used to identify the input event later in the callback.
+         * @param callback A callback used to receive the dispatch result. Cannot be {@code null}.
+         * @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be
+         *            {@code null}.
+         * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
+         *         {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
+         *         {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
+         *         be invoked later.
+         * @hide
+         */
+        public int dispatchInputEvent(@NonNull InputEvent event, Object token,
+                @NonNull FinishedInputEventCallback callback, @NonNull Handler handler) {
+            Preconditions.checkNotNull(event);
+            Preconditions.checkNotNull(callback);
+            Preconditions.checkNotNull(handler);
+            synchronized (mHandler) {
+                if (mInputChannel == null) {
+                    return DISPATCH_NOT_HANDLED;
+                }
+                PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
+                if (Looper.myLooper() == Looper.getMainLooper()) {
+                    // Already running on the main thread so we can send the event immediately.
+                    return sendInputEventOnMainLooperLocked(p);
+                }
+
+                // Post the event to the main thread.
+                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
+                msg.setAsynchronous(true);
+                mHandler.sendMessage(msg);
+                return DISPATCH_IN_PROGRESS;
+            }
+        }
+
+        private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
+                FinishedInputEventCallback callback, Handler handler) {
+            PendingEvent p = mPendingEventPool.acquire();
+            if (p == null) {
+                p = new PendingEvent();
+            }
+            p.mEvent = event;
+            p.mEventToken = token;
+            p.mCallback = callback;
+            p.mEventHandler = handler;
+            return p;
+        }
+
+        /**
+         * Callback that is invoked when an input event that was dispatched to this session has been
+         * finished.
+         *
+         * @hide
+         */
+        public interface FinishedInputEventCallback {
+            /**
+             * Called when the dispatched input event is finished.
+             *
+             * @param token A token passed to {@link #dispatchInputEvent}.
+             * @param handled {@code true} if the dispatched input event was handled properly.
+             *            {@code false} otherwise.
+             */
+            void onFinishedInputEvent(Object token, boolean handled);
+        }
+
         private final class InputEventHandler extends Handler {
             public static final int MSG_SEND_INPUT_EVENT = 1;
             public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
@@ -639,23 +1045,6 @@
             mPendingEventPool.release(p);
         }
 
-        /**
-         * Callback that is invoked when an input event that was dispatched to this session has been
-         * finished.
-         *
-         * @hide
-         */
-        public interface FinishedInputEventCallback {
-            /**
-             * Called when the dispatched input event is finished.
-             *
-             * @param token A token passed to {@link #dispatchInputEvent}.
-             * @param handled {@code true} if the dispatched input event was handled properly.
-             *            {@code false} otherwise.
-             */
-            void onFinishedInputEvent(Object token, boolean handled);
-        }
-
         private final class TvInputEventSender extends InputEventSender {
             TvInputEventSender(InputChannel inputChannel, Looper looper) {
                 super(inputChannel, looper);
@@ -730,6 +1119,58 @@
         public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
         }
 
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentVideoBounds} is
+         * called.
+         *
+         * @param session A {@link TvAdService.Session} associated with this callback.
+         */
+        public void onRequestCurrentVideoBounds(Session session) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentChannelUri} is
+         * called.
+         *
+         * @param session A {@link TvAdService.Session} associated with this callback.
+         */
+        public void onRequestCurrentChannelUri(Session session) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestTrackInfoList} is
+         * called.
+         *
+         * @param session A {@link TvAdService.Session} associated with this callback.
+         */
+        public void onRequestTrackInfoList(Session session) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentTvInputId} is
+         * called.
+         *
+         * @param session A {@link TvAdService.Session} associated with this callback.
+         */
+        public void onRequestCurrentTvInputId(Session session) {
+        }
+
+        /**
+         * This is called when
+         * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} is
+         * called.
+         *
+         * @param session A {@link TvAdService.Session} associated with this callback.
+         * @param signingId the ID to identify the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc.
+         * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+         * @param data the original bytes to be signed.
+         */
+        public void onRequestSigning(
+                Session session, String signingId, String algorithm, String alias, byte[] data) {
+        }
+
     }
 
     /**
@@ -810,6 +1251,62 @@
                 }
             });
         }
+
+        void postRequestCurrentVideoBounds() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestCurrentVideoBounds(mSession);
+                }
+            });
+        }
+
+        void postRequestCurrentChannelUri() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestCurrentChannelUri(mSession);
+                }
+            });
+        }
+
+        void postRequestTrackInfoList() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestTrackInfoList(mSession);
+                }
+            });
+        }
+
+        void postRequestCurrentTvInputId() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestCurrentTvInputId(mSession);
+                }
+            });
+        }
+
+        void postRequestSigning(String id, String algorithm, String alias, byte[] data) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestSigning(mSession, id, algorithm, alias, data);
+                }
+            });
+        }
+
+        void postTvAdSessionData(String type, Bundle data) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mSession.getInputSession() != null) {
+                        mSession.getInputSession().notifyTvAdSessionData(type, data);
+                    }
+                }
+            });
+        }
     }
 
     private static final class TvAdServiceCallbackRecord {
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 5d81837..4d8f5c8b 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -31,6 +31,7 @@
 import android.graphics.Rect;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -132,6 +133,9 @@
 
     /**
      * Called when app link command is received.
+     *
+     * @see TvAdManager#sendAppLinkCommand(String, Bundle)
+     * @hide
      */
     public void onAppLinkCommand(@NonNull Bundle command) {
     }
@@ -279,6 +283,143 @@
             onResetAdService();
         }
 
+        /**
+         * Requests the bounds of the current video.
+         * @hide
+         */
+        @CallSuper
+        public void requestCurrentVideoBounds() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestCurrentVideoBounds");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestCurrentVideoBounds();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestCurrentVideoBounds", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Requests the URI of the current channel.
+         * @hide
+         */
+        @CallSuper
+        public void requestCurrentChannelUri() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestCurrentChannelUri");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestCurrentChannelUri();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestCurrentChannelUri", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Requests the list of {@link TvTrackInfo}.
+         * @hide
+         */
+        @CallSuper
+        public void requestTrackInfoList() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestTrackInfoList");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestTrackInfoList();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestTrackInfoList", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Requests current TV input ID.
+         *
+         * @see android.media.tv.TvInputInfo
+         * @hide
+         */
+        @CallSuper
+        public void requestCurrentTvInputId() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestCurrentTvInputId");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestCurrentTvInputId();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestCurrentTvInputId", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Requests signing of the given data.
+         *
+         * <p>This is used when the corresponding server of the AD service app requires signing
+         * during handshaking, and the service doesn't have the built-in private key. The private
+         * key is provided by the content providers and pre-built in the related app, such as TV
+         * app.
+         *
+         * @param signingId the ID to identify the request. When a result is received, this ID can
+         *                  be used to correlate the result with the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc. The name is from standards like
+         *                  FIPS PUB 186-4 and PKCS #1.
+         * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+         * @param data the original bytes to be signed.
+         *
+         * @see #onSigningResult(String, byte[])
+         */
+        @CallSuper
+        public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
+                @NonNull String alias, @NonNull byte[] data) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestSigning");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestSigning(signingId, algorithm, alias, data);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestSigning", e);
+                    }
+                }
+            });
+        }
+
         @Override
         public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
             return false;
@@ -470,6 +611,19 @@
         }
 
         /**
+         * Called when data from the linked {@link android.media.tv.TvInputService} is received.
+         *
+         * @param type the type of the data
+         * @param data a bundle contains the data received
+         * @see android.media.tv.TvInputService.Session#notifyTvAdSessionData(String, Bundle)
+         * @see android.media.tv.ad.TvAdView#setTvView(TvView)
+         * @hide
+         */
+        public void onTvInputSessionData(
+                @NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
+        }
+
+        /**
          * Called when the size of the media view is changed by the application.
          *
          * <p>This is always called at least once when the session is created regardless of whether
@@ -497,6 +651,33 @@
         }
 
         /**
+         * Notifies data related to this session to corresponding linked
+         * {@link android.media.tv.TvInputService} object via TvView.
+         *
+         * @param type data type
+         * @param data the related data values
+         * @see TvAdView#setTvView(TvView)
+         * @hide
+         */
+        public void notifyTvAdSessionData(
+                @NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTvAdSessionData");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTvAdSessionData(type, data);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTvAdSessionData", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Takes care of dispatching incoming input events and tells whether the event was handled.
          */
         int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
@@ -594,6 +775,10 @@
             onTvMessage(type, data);
         }
 
+        void notifyTvInputSessionData(String type, Bundle data) {
+            onTvInputSessionData(type, data);
+        }
+
         private void executeOrPostRunnableOnMainThread(Runnable action) {
             synchronized (mLock) {
                 if (mSessionCallback == null) {
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index ec23b7c..604dbd5 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -28,17 +28,21 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvTrackInfo;
 import android.media.tv.TvView;
+import android.media.tv.ad.TvAdManager.Session.FinishedInputEventCallback;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Xml;
+import android.view.InputEvent;
+import android.view.KeyEvent;
 import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewRootImpl;
 
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -88,6 +92,7 @@
 
     private boolean mMediaViewCreated;
     private Rect mMediaViewFrame;
+    private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
 
 
 
@@ -327,6 +332,109 @@
         mSession.dispatchSurfaceChanged(format, width, height);
     }
 
+    private final FinishedInputEventCallback mFinishedInputEventCallback =
+            new FinishedInputEventCallback() {
+                @Override
+                public void onFinishedInputEvent(Object token, boolean handled) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled="
+                                + handled + ")");
+                    }
+                    if (handled) {
+                        return;
+                    }
+                    // TODO: Re-order unhandled events.
+                    InputEvent event = (InputEvent) token;
+                    if (dispatchUnhandledInputEvent(event)) {
+                        return;
+                    }
+                    ViewRootImpl viewRootImpl = getViewRootImpl();
+                    if (viewRootImpl != null) {
+                        viewRootImpl.dispatchUnhandledInputEvent(event);
+                    }
+                }
+            };
+
+    /**
+     * Dispatches an unhandled input event to the next receiver.
+     *
+     * It gives the host application a chance to dispatch the unhandled input events.
+     *
+     * @param event The input event.
+     * @return {@code true} if the event was handled by the view, {@code false} otherwise.
+     * @hide
+     */
+    public boolean dispatchUnhandledInputEvent(@NonNull InputEvent event) {
+        if (mOnUnhandledInputEventListener != null) {
+            if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
+                return true;
+            }
+        }
+        return onUnhandledInputEvent(event);
+    }
+
+    /**
+     * Called when an unhandled input event also has not been handled by the user provided
+     * callback. This is the last chance to handle the unhandled input event in the
+     * TvAdView.
+     *
+     * @param event The input event.
+     * @return If you handled the event, return {@code true}. If you want to allow the event to be
+     *         handled by the next receiver, return {@code false}.
+     */
+    public boolean onUnhandledInputEvent(@NonNull InputEvent event) {
+        return false;
+    }
+
+    /**
+     * Sets a listener to be invoked when an input event is not handled
+     * by the TV AD service.
+     *
+     * @param listener The callback to be invoked when the unhandled input event is received.
+     * @hide
+     */
+    public void setOnUnhandledInputEventListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnUnhandledInputEventListener listener) {
+        mOnUnhandledInputEventListener = listener;
+        // TODO: handle CallbackExecutor
+    }
+
+    /**
+     * Gets the {@link OnUnhandledInputEventListener}.
+     * <p>Returns {@code null} if the listener is not set or is cleared.
+     *
+     * @see #setOnUnhandledInputEventListener(Executor, OnUnhandledInputEventListener)
+     * @see #clearOnUnhandledInputEventListener()
+     * @hide
+     */
+    @Nullable
+    public OnUnhandledInputEventListener getOnUnhandledInputEventListener() {
+        return mOnUnhandledInputEventListener;
+    }
+
+    /**
+     * Clears the {@link OnUnhandledInputEventListener}.
+     * @hide
+     */
+    public void clearOnUnhandledInputEventListener() {
+        mOnUnhandledInputEventListener = null;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
+        if (super.dispatchKeyEvent(event)) {
+            return true;
+        }
+        if (mSession == null) {
+            return false;
+        }
+        InputEvent copiedEvent = event.copy();
+        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
+                mHandler);
+        return ret != TvAdManager.Session.DISPATCH_NOT_HANDLED;
+    }
+
     /**
      * Prepares the AD service of corresponding {@link TvAdService}.
      *
@@ -504,6 +612,24 @@
     }
 
     /**
+     * Interface definition for a callback to be invoked when the unhandled input event is received.
+     * @hide
+     */
+    public interface OnUnhandledInputEventListener {
+        /**
+         * Called when an input event was not handled by the TV AD service.
+         *
+         * <p>This is called asynchronously from where the event is dispatched. It gives the host
+         * application a chance to handle the unhandled input events.
+         *
+         * @param event The input event.
+         * @return If you handled the event, return {@code true}. If you want to allow the event to
+         *         be handled by the next receiver, return {@code false}.
+         */
+        boolean onUnhandledInputEvent(@NonNull InputEvent event);
+    }
+
+    /**
      * Sets the callback to be invoked when an event is dispatched to this TvAdView.
      *
      * @param callback the callback to receive events. MUST NOT be {@code null}.
@@ -606,6 +732,117 @@
             mUseRequestedSurfaceLayout = true;
             requestLayout();
         }
+
+        @Override
+        public void onRequestCurrentVideoBounds(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestCurrentVideoBounds");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestCurrentVideoBounds - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestCurrentVideoBounds(mServiceId);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
+        public void onRequestCurrentChannelUri(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestCurrentChannelUri");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestCurrentChannelUri - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestCurrentChannelUri(mServiceId);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
+        public void onRequestTrackInfoList(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestTrackInfoList");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestTrackInfoList - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestTrackInfoList(mServiceId);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
+        public void onRequestCurrentTvInputId(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestCurrentTvInputId");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestCurrentTvInputId - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestCurrentTvInputId(mServiceId);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
+        public void onRequestSigning(TvAdManager.Session session, String id, String algorithm,
+                String alias, byte[] data) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestSigning");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestSigning - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestSigning(mServiceId, id, algorithm, alias, data);
+                            }
+                        }
+                    });
+                }
+            }
+        }
     }
 
     /**
@@ -613,5 +850,55 @@
      * @hide
      */
     public abstract static class TvAdCallback {
+
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentVideoBounds()}
+         * is called.
+         *
+         * @param serviceId The ID of the TV AD service bound to this view.
+         */
+        public void onRequestCurrentVideoBounds(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentChannelUri()} is
+         * called.
+         *
+         * @param serviceId The ID of the AD service bound to this view.
+         */
+        public void onRequestCurrentChannelUri(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestTrackInfoList()} is called.
+         *
+         * @param serviceId The ID of the AD service bound to this view.
+         */
+        public void onRequestTrackInfoList(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#requestCurrentTvInputId()} is called.
+         *
+         * @param serviceId The ID of the AD service bound to this view.
+         */
+        public void onRequestCurrentTvInputId(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when
+         * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} is called.
+         *
+         * @param serviceId The ID of the AD service bound to this view.
+         * @param signingId the ID to identify the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc.
+         * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+         * @param data the original bytes to be signed.
+         *
+         */
+        public void onRequestSigning(@NonNull String serviceId, @NonNull String signingId,
+                @NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) {
+        }
     }
 }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 7b58531..1404d7c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -63,6 +63,8 @@
     void onRequestTvRecordingInfoList(in int type, int seq);
     void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data,
             int seq);
+    void onRequestSigning2(in String id, in String algorithm, in String host,
+            int port, in byte[] data, int seq);
     void onRequestCertificate(in String host, int port, int seq);
     void onAdRequest(in AdRequest request, int Seq);
 }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index cb89181..3c91a3e 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -61,6 +61,7 @@
     void onRequestTvRecordingInfo(in String recordingId);
     void onRequestTvRecordingInfoList(in int type);
     void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
+    void onRequestSigning2(in String id, in String algorithm, in String host, int port, in byte[] data);
     void onRequestCertificate(in String host, int port);
     void onAdRequest(in AdRequest request);
 }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 011744f..498eec6 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -657,6 +657,19 @@
             }
 
             @Override
+            public void onRequestSigning2(
+                    String id, String algorithm, String host, int port, byte[] data, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestSigning(id, algorithm, host, port, data);
+                }
+            }
+
+            @Override
             public void onRequestCertificate(String host, int port, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -2258,6 +2271,17 @@
             });
         }
 
+        void postRequestSigning(String id, String algorithm, String host, int port,
+                byte[] data) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestSigning(mSession, id, algorithm, host,
+                            port, data);
+                }
+            });
+        }
+
         void postRequestCertificate(String host, int port) {
             mHandler.post(new Runnable() {
                 @Override
@@ -2609,6 +2633,25 @@
         }
 
         /**
+         * This is called when
+         * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param signingId the ID to identify the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc.
+         * @param host The host of the SSL CLient Authentication Server
+         * @param port The port of the SSL Client Authentication Server
+         * @param data the original bytes to be signed.
+         * @hide
+         */
+        public void onRequestSigning(
+                Session session, String signingId, String algorithm, String host,
+                int port, byte[] data) {
+        }
+
+        /**
          * This is called when the service requests a SSL certificate for client validation.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 054b272..7b6dc38 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -1645,6 +1645,50 @@
         }
 
         /**
+         * Requests signing of the given data.
+         *
+         * <p>This is used when the corresponding server of the broadcast-independent interactive
+         * app requires signing during handshaking, and the interactive app service doesn't have
+         * the built-in private key. The private key is provided by the content providers and
+         * pre-built in the related app, such as TV app.
+         *
+         * @param signingId the ID to identify the request. When a result is received, this ID can
+         *                  be used to correlate the result with the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc. The name is from standards like
+         *                  FIPS PUB 186-4 and PKCS #1.
+         * @param host the host of the SSL client authentication server.
+         * @param port the port of the SSL client authentication server.
+         * @param data the original bytes to be signed.
+         *
+         * @see #onSigningResult(String, byte[])
+         * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle)
+         * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS
+         * @hide
+         */
+        @CallSuper
+        public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
+                @NonNull String host, int port, @NonNull byte[] data) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestSigning");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestSigning2(signingId, algorithm,
+                                    host, port, data);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestSigning", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Requests a SSL certificate for client validation.
          *
          * @param host the host name of the SSL authentication server.
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 3524f8c..f8b640c7 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -17,7 +17,7 @@
     method @FlaggedApi("android.nfc.enable_nfc_charging") public void registerWlcStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.WlcStateListener);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler);
     method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean);
-    method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderMode(boolean);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderModePollingEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean);
     method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setWlcEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 11eb97b..4d56c11 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -1764,7 +1764,9 @@
     private static final int ENABLE_POLLING_FLAGS = 0x0000;
 
     /**
-     * Privileged API to enable disable reader polling.
+     * Privileged API to enable or disable reader polling.
+     * Unlike {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}, this API does not
+     * need a foreground activity to control reader mode parameters
      * Note: Use with caution! The app is responsible for ensuring that the polling state is
      * returned to normal.
      *
@@ -1778,14 +1780,14 @@
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
     @SuppressLint("VisiblySynchronized")
-    public void setReaderMode(boolean enablePolling) {
+    public void setReaderModePollingEnabled(boolean enable) {
         synchronized (sLock) {
             if (!sHasNfcFeature) {
                 throw new UnsupportedOperationException();
             }
         }
         Binder token = new Binder();
-        int flags = enablePolling ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS;
+        int flags = enable ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS;
         try {
             NfcAdapter.sService.setReaderMode(token, null, flags, null);
         } catch (RemoteException e) {
diff --git a/packages/CredentialManager/res/drawable/more_horiz_24px.xml b/packages/CredentialManager/res/drawable/more_horiz_24px.xml
index 7b235f8..0100718 100644
--- a/packages/CredentialManager/res/drawable/more_horiz_24px.xml
+++ b/packages/CredentialManager/res/drawable/more_horiz_24px.xml
@@ -3,6 +3,7 @@
     android:height="24dp"
     android:viewportWidth="960"
     android:viewportHeight="960"
+    android:contentDescription="@string/more_options_content_description"
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 929756c..fdda9ea 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -17,6 +17,7 @@
                 android:id="@android:id/content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:minWidth="@dimen/dropdown_touch_target_min_width"
                 android:orientation="horizontal"
                 android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
                 android:elevation="3dp">
@@ -27,7 +28,7 @@
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
         android:layout_alignParentStart="true"
-        android:contentDescription="@string/provider_icon_content_description"
+        android:contentDescription="@string/more_options_content_description"
         android:background="@null"/>
     <TextView
         android:id="@android:id/text1"
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 1fe5e0e..c7c2fda 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -17,6 +17,7 @@
                 android:id="@android:id/content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:minWidth="@dimen/dropdown_touch_target_min_width"
                 android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
                 android:elevation="3dp">
 
@@ -24,7 +25,6 @@
             android:id="@android:id/icon1"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:contentDescription="@string/provider_icon_content_description"
             android:layout_centerVertical="true"
             android:layout_alignParentStart="true"
             android:background="@null"/>
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 3a8c78f..53852cb 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -27,4 +27,5 @@
     <dimen name="autofill_dropdown_textview_max_width">230dp</dimen>
     <dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
     <integer name="autofill_max_visible_datasets">3</integer>
+    <dimen name="dropdown_touch_target_min_width">48dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index f98164b..82b47a9 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -172,5 +172,5 @@
   <!-- Strings for dropdown presentation. -->
   <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] -->
   <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string>
-  <string name="provider_icon_content_description">Credential provider icon</string>
+  <string name="more_options_content_description">More</string>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index 68f1c86..02afc54 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -74,6 +74,8 @@
                     setMaxHeightMethodName,
                     context.resources.getDimensionPixelSize(
                             com.android.credentialmanager.R.dimen.autofill_icon_size));
+            remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo
+                    .providerDisplayName);
             val drawableId =
                     com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one
             remoteViews.setInt(
diff --git a/packages/CtsShim/OWNERS b/packages/CtsShim/OWNERS
index 9419771..f5741a0 100644
--- a/packages/CtsShim/OWNERS
+++ b/packages/CtsShim/OWNERS
@@ -1,3 +1,2 @@
-ioffe@google.com
-toddke@google.com
-patb@google.com
\ No newline at end of file
+include /PACKAGE_MANAGER_OWNERS
+ioffe@google.com
\ No newline at end of file
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 5a21d59..09e0d61 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -75,19 +75,15 @@
             </intent-filter>
         </activity>
 
-        <!-- NOTE: the workaround to fix the screen flash problem. Remember to check the problem
-            is resolved for new implementation -->
         <activity android:name=".InstallStaging"
-                  android:theme="@style/Theme.AlertDialogActivity.NoDim"
-                  android:exported="false" />
+                android:exported="false" />
 
         <activity android:name=".DeleteStagedFileOnResult"
             android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
             android:exported="false" />
 
         <activity android:name=".PackageInstallerActivity"
-                  android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
-                  android:exported="false" />
+                android:exported="false" />
 
         <activity android:name=".InstallInstalling"
                 android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
diff --git a/packages/PackageInstaller/OWNERS b/packages/PackageInstaller/OWNERS
index 2736870..acbdff8 100644
--- a/packages/PackageInstaller/OWNERS
+++ b/packages/PackageInstaller/OWNERS
@@ -1,5 +1,4 @@
-svetoslavganov@google.com
+# Bug component: 1002434
 include /PACKAGE_MANAGER_OWNERS
 
-# For automotive related changes
-rogerxue@google.com
+sumedhsen@google.com
\ No newline at end of file
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index 811fa73..9a06229 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -32,9 +32,4 @@
         <item name="android:windowNoTitle">true</item>
     </style>
 
-    <style name="Theme.AlertDialogActivity.NoDim"
-        parent="@style/Theme.AlertDialogActivity.NoActionBar">
-        <item name="android:backgroundDimAmount">0</item>
-    </style>
-
 </resources>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index ceb580d..7dc157f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -308,7 +308,6 @@
     }
 
     private void initiateInstall() {
-        bindUi();
         String pkgName = mPkgInfo.packageName;
         // Check if there is already a package on the device with this name
         // but it has been renamed to something else.
@@ -448,6 +447,7 @@
         if (mAppSnippet != null) {
             // load placeholder layout with OK button disabled until we override this layout in
             // startInstallConfirm
+            bindUi();
             checkIfAllowedAndInitiateInstall();
         }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
index b2a8b87..960ebcc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
@@ -21,6 +21,7 @@
 
 data class CardButton(
     val text: String,
+    val contentDescription: String? = null,
     val onClick: () -> Unit,
 )
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index c7845fa..700fa48 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -45,6 +45,8 @@
 import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.debug.UiModePreviews
 import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -182,7 +184,11 @@
 
 @Composable
 private fun Button(button: CardButton, color: Color) {
-    TextButton(onClick = button.onClick) {
+    TextButton(
+        onClick = button.onClick,
+        modifier =
+            Modifier.semantics { button.contentDescription?.let { this.contentDescription = it } }
+    ) {
         Text(text = button.text, color = color)
     }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
index beb9433..b5b2525 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
@@ -36,8 +36,7 @@
 
 @RunWith(AndroidJUnit4::class)
 class SettingsCardTest {
-    @get:Rule
-    val composeTestRule = createComposeRule()
+    @get:Rule val composeTestRule = createComposeRule()
 
     private val context: Context = ApplicationProvider.getApplicationContext()
 
@@ -76,9 +75,7 @@
                 CardModel(
                     title = "",
                     text = "",
-                    buttons = listOf(
-                        CardButton(text = TEXT) {}
-                    ),
+                    buttons = listOf(CardButton(text = TEXT) {}),
                 )
             )
         }
@@ -94,9 +91,7 @@
                 CardModel(
                     title = "",
                     text = "",
-                    buttons = listOf(
-                        CardButton(text = TEXT) { buttonClicked = true }
-                    ),
+                    buttons = listOf(CardButton(text = TEXT) { buttonClicked = true }),
                 )
             )
         }
@@ -107,6 +102,25 @@
     }
 
     @Test
+    fun settingsCard_buttonHaveContentDescription() {
+        composeTestRule.setContent {
+            SettingsCard(
+                CardModel(
+                    title = "",
+                    text = "",
+                    buttons = listOf(CardButton(
+                        text = TEXT,
+                        contentDescription = CONTENT_DESCRIPTION,
+                        ) {}
+                    ),
+                )
+            )
+        }
+
+        composeTestRule.onNodeWithContentDescription(CONTENT_DESCRIPTION).assertIsDisplayed()
+    }
+
+    @Test
     fun settingsCard_dismiss() {
         composeTestRule.setContent {
             var isVisible by remember { mutableStateOf(true) }
@@ -130,5 +144,6 @@
     private companion object {
         const val TITLE = "Title"
         const val TEXT = "Text"
+        const val CONTENT_DESCRIPTION = "content-description"
     }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 38ec931..4c255a5 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -107,6 +107,7 @@
                 Settings.System.TOUCHPAD_POINTER_SPEED,
                 Settings.System.TOUCHPAD_NATURAL_SCROLLING,
                 Settings.System.TOUCHPAD_TAP_TO_CLICK,
+                Settings.System.TOUCHPAD_TAP_DRAGGING,
                 Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE,
                 Settings.System.CAMERA_FLASH_NOTIFICATION,
                 Settings.System.SCREEN_FLASH_NOTIFICATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 98941c7..011b42f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -209,6 +209,7 @@
         VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.TOUCHPAD_TAP_DRAGGING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_RIGHT_CLICK_ZONE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.LOCK_TO_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1670a70..1e146a5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -3002,6 +3002,9 @@
         dumpSetting(s, p,
                 Settings.System.TOUCHPAD_TAP_TO_CLICK,
                 SystemSettingsProto.Touchpad.TAP_TO_CLICK);
+        dumpSetting(s, p,
+                Settings.System.TOUCHPAD_TAP_DRAGGING,
+                SystemSettingsProto.Touchpad.TAP_DRAGGING);
         p.end(touchpadToken);
 
         dumpSetting(s, p,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 54ab5d1..0050676 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1083,5 +1083,25 @@
         <!-- Allow SystemUI to listen for the capabilities defined in the linked xml -->
         <property android:name="android.net.PROPERTY_SELF_CERTIFIED_CAPABILITIES"
                   android:value="@xml/self_certified_network_capabilities_both" />
+
+
+        <service
+            android:name="com.android.systemui.dreams.homecontrols.HomeControlsDreamService"
+            android:exported="false"
+            android:enabled="false"
+            android:label="@string/home_controls_dream_label"
+            android:description="@string/home_controls_dream_description"
+            android:permission="android.permission.BIND_DREAM_SERVICE"
+            android:icon="@drawable/controls_icon"
+            >
+
+            <intent-filter>
+                <action android:name="android.service.dreams.DreamService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data
+                android:name="android.service.dream"
+                android:resource="@xml/home_controls_dream_metadata" />
+        </service>
     </application>
 </manifest>
diff --git a/packages/SystemUI/aconfig/cross_device_control.aconfig b/packages/SystemUI/aconfig/cross_device_control.aconfig
new file mode 100644
index 0000000..d3f14c1
--- /dev/null
+++ b/packages/SystemUI/aconfig/cross_device_control.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.systemui"
+
+flag {
+    name: "legacy_le_audio_sharing"
+    namespace: "pixel_cross_device_control"
+    description: "Gates the legacy le audio sharing UI."
+    bug: "322295262"
+}
+
+flag {
+    name: "enable_personal_le_audio_sharing"
+    namespace: "pixel_cross_device_control"
+    description: "Gates the personal le audio sharing UI in UMO."
+    bug: "322295480"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a2530d5..3db99f28 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -59,14 +59,6 @@
 }
 
 flag {
-    name: "notification_lifetime_extension_refactor"
-    namespace: "systemui"
-    description: "Enables moving notification lifetime extension management from SystemUI to "
-        "Notification Manager Service"
-    bug: "299448097"
-}
-
-flag {
     name: "notifications_live_data_store_refactor"
     namespace: "systemui"
     description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. "
@@ -371,3 +363,24 @@
     description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths"
     bug: "278086361"
 }
+
+flag {
+   name: "enable_keyguard_compose"
+   namespace: "systemui"
+   description: "Enables the compose version of keyguard."
+   bug: "301968149"
+}
+
+flag {
+   name: "enable_contextual_tip_for_power_off"
+   namespace: "systemui"
+   description: "Enables on-screen contextual tip about how to power off or restart phone"
+   bug: "322891421"
+}
+
+flag {
+   name: "enable_contextual_tip_for_take_screenshot"
+   namespace: "systemui"
+   description: "Enables on-screen contextual tip about how to take screenshot."
+   bug: "322891421"
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index f9b91cf..bd539a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -20,6 +20,7 @@
 
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.TextField
@@ -36,16 +37,21 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformIconButton
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
+import com.android.systemui.res.R
 
 /** UI for the input part of a password-requiring version of the bouncer. */
 @Composable
@@ -64,6 +70,7 @@
     val password: String by viewModel.password.collectAsState()
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+    val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
 
     DisposableEffect(Unit) {
         viewModel.onShown()
@@ -116,5 +123,28 @@
                         false
                     }
                 },
+        trailingIcon =
+            if (isImeSwitcherButtonVisible) {
+                { ImeSwitcherButton(viewModel, color) }
+            } else null
+    )
+}
+
+/** Button for changing the password input method (IME). */
+@Composable
+private fun ImeSwitcherButton(
+    viewModel: PasswordBouncerViewModel,
+    color: Color,
+) {
+    val context = LocalContext.current
+    PlatformIconButton(
+        onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
+        iconResource = R.drawable.ic_lockscreen_ime,
+        contentDescription = stringResource(R.string.accessibility_ime_switch_button),
+        colors =
+            IconButtonDefaults.filledIconButtonColors(
+                contentColor = color,
+                containerColor = Color.Transparent,
+            )
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index ff5a698..92bc1f1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -124,7 +124,7 @@
 
 // TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
 fun CommunalSceneKey.toTransitionSceneKey(): SceneKey {
-    return SceneKey(name = toString(), identity = this)
+    return SceneKey(debugName = toString(), identity = this)
 }
 
 /**
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 42fcd13..5e27d82 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -21,19 +21,27 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.notifications.ui.composable.NotificationStack
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 
+@SysUISingleton
 class NotificationSection
 @Inject
 constructor(
@@ -42,22 +50,41 @@
     controller: NotificationStackScrollLayoutController,
     sceneContainerFlags: SceneContainerFlags,
     sharedNotificationContainer: SharedNotificationContainer,
+    sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
     notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
     ambientState: AmbientState,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    @Main mainDispatcher: CoroutineDispatcher,
 ) {
     init {
-        if (sceneContainerFlags.flexiNotifsEnabled()) {
+        if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) {
+            // This scene container section moves the NSSL to the SharedNotificationContainer. This
+            //  also requires that SharedNotificationContainer gets moved to the SceneWindowRootView
+            //  by the SceneWindowRootViewBinder.
+            // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled,
+            //  NSSL is moved into this container by the NotificationStackScrollLayoutSection.
             (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout)
             sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
 
-            NotificationStackAppearanceViewBinder.bind(
-                context,
+            SharedNotificationContainerBinder.bind(
                 sharedNotificationContainer,
-                notificationStackAppearanceViewModel,
-                ambientState,
+                sharedNotificationContainerViewModel,
+                sceneContainerFlags,
                 controller,
+                notificationStackSizeCalculator,
+                mainDispatcher,
             )
+
+            if (sceneContainerFlags.flexiNotifsEnabled()) {
+                NotificationStackAppearanceViewBinder.bind(
+                    context,
+                    sharedNotificationContainer,
+                    notificationStackAppearanceViewModel,
+                    ambientState,
+                    controller,
+                )
+            }
         }
     }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
new file mode 100644
index 0000000..2ba78cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.notifications.ui.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+
+/**
+ * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
+ * following way:
+ * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and
+ *   then allows scrolling of the children (usually the content).
+ * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and
+ *   then resets the [scrimOffset] to [maxScrimOffset].
+ */
+fun NotificationScrimNestedScrollConnection(
+    scrimOffset: () -> Float,
+    onScrimOffsetChanged: (Float) -> Unit,
+    minScrimOffset: () -> Float,
+    maxScrimOffset: Float,
+    contentHeight: () -> Float,
+    minVisibleScrimHeight: () -> Float,
+): PriorityNestedScrollConnection {
+    return PriorityNestedScrollConnection(
+        orientation = Orientation.Vertical,
+        // scrolling up and inner content is taller than the scrim, so scrim needs to
+        // expand; content can scroll once scrim is at the minScrimOffset.
+        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+            offsetAvailable < 0 &&
+                offsetBeforeStart == 0f &&
+                contentHeight() > minVisibleScrimHeight() &&
+                scrimOffset() > minScrimOffset()
+        },
+        // scrolling down and content is done scrolling to top. After that, the scrim
+        // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
+        canStartPostScroll = { offsetAvailable, _ ->
+            offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+        },
+        canStartPostFling = { false },
+        canContinueScroll = {
+            val currentHeight = scrimOffset()
+            minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
+        },
+        canScrollOnFling = true,
+        onStart = { /* do nothing */},
+        onScroll = { offsetAvailable ->
+            val currentHeight = scrimOffset()
+            val amountConsumed =
+                if (offsetAvailable > 0) {
+                    val amountLeft = maxScrimOffset - currentHeight
+                    offsetAvailable.coerceAtMost(amountLeft)
+                } else {
+                    val amountLeft = minScrimOffset() - currentHeight
+                    offsetAvailable.coerceAtLeast(amountLeft)
+                }
+            onScrimOffsetChanged(currentHeight + amountConsumed)
+            amountConsumed
+        },
+        // Don't consume the velocity on pre/post fling
+        onStop = { 0f },
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index e835d3e..0e08a19 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -20,32 +20,53 @@
 import android.util.Log
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.height
 import com.android.systemui.notifications.ui.composable.Notifications.Form
+import com.android.systemui.scene.ui.composable.Gone
+import com.android.systemui.scene.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
 
@@ -100,33 +121,109 @@
 @Composable
 fun SceneScope.NotificationScrollingStack(
     viewModel: NotificationsPlaceholderViewModel,
+    maxScrimTop: () -> Float,
     modifier: Modifier = Modifier,
 ) {
+    val density = LocalDensity.current
     val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
-
-    val contentHeight by viewModel.intrinsicContentHeight.collectAsState()
-
     val expansionFraction by viewModel.expandFraction.collectAsState(0f)
 
-    Box(
-        modifier =
-            modifier
-                .verticalNestedScrollToScene()
-                .fillMaxWidth()
-                .element(Notifications.Elements.NotificationScrim)
-                .graphicsLayer {
-                    shape = RoundedCornerShape(cornerRadius.dp)
-                    clip = true
-                    alpha = expansionFraction
-                }
-                .background(MaterialTheme.colorScheme.surface)
-                .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
-    ) {
-        NotificationPlaceholder(
-            viewModel = viewModel,
-            form = Form.Stack,
-            modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() }
+    val navBarHeight =
+        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+    val statusBarHeight =
+        with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
+    val displayCutoutHeight =
+        with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
+    val screenHeight =
+        with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
+            navBarHeight +
+            maxOf(statusBarHeight, displayCutoutHeight)
+
+    val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+
+    // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
+    // calculated in minScrimOffset. The scrim is the same height as the screen minus the
+    // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
+    // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
+    // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
+    // entire height of the scrim is visible on screen.
+    val scrimOffset = remember { mutableStateOf(0f) }
+
+    val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
+
+    // The minimum offset for the scrim. The scrim is considered fully expanded when it
+    // is at this offset.
+    val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }
+
+    // The height of the scrim visible on screen when it is in its resting (collapsed) state.
+    val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
+
+    // we are not scrolled to the top unless the scrim is at its maximum offset.
+    LaunchedEffect(viewModel, scrimOffset) {
+        snapshotFlow { scrimOffset.value >= 0f }
+            .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
+    }
+
+    // if contentHeight drops below minimum visible scrim height while scrim is
+    // expanded, reset scrim offset.
+    LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+        snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
+            .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+    }
+
+    Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
+        Spacer(
+            modifier =
+                Modifier.fillMaxSize()
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                    }
+                    .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
         )
+        Box(
+            modifier =
+                Modifier.fillMaxSize()
+                    .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                        alpha =
+                            if (layoutState.isTransitioningBetween(Gone, Shade)) {
+                                (expansionFraction / 0.3f).coerceAtMost(1f)
+                            } else 1f
+                    }
+                    .background(MaterialTheme.colorScheme.surface)
+                    .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
+        ) {
+            NotificationPlaceholder(
+                viewModel = viewModel,
+                form = Form.Stack,
+                modifier =
+                    Modifier.verticalNestedScrollToScene(
+                            topBehavior = NestedScrollBehavior.EdgeWithPreview,
+                        )
+                        .nestedScroll(
+                            remember(
+                                scrimOffset,
+                                maxScrimTop,
+                                minScrimTop,
+                            ) {
+                                NotificationScrimNestedScrollConnection(
+                                    scrimOffset = { scrimOffset.value },
+                                    onScrimOffsetChanged = { scrimOffset.value = it },
+                                    minScrimOffset = minScrimOffset,
+                                    maxScrimOffset = 0f,
+                                    contentHeight = { contentHeight.value },
+                                    minVisibleScrimHeight = minVisibleScrimHeight,
+                                )
+                            }
+                        )
+                        .verticalScroll(rememberScrollState())
+                        .fillMaxWidth()
+                        .height { (contentHeight.value + navBarHeight).roundToInt() },
+            )
+        }
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index bbfe0fd..5531f9c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -46,6 +47,11 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
@@ -56,7 +62,7 @@
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.footer.ui.compose.FooterActions
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.SceneKey
@@ -116,6 +122,8 @@
     statusBarIconController: StatusBarIconController,
     modifier: Modifier = Modifier,
 ) {
+    val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()
+
     // TODO(b/280887232): implement the real UI.
     Box(modifier = modifier.fillMaxSize()) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -234,10 +242,32 @@
                 }
             }
         }
-        HeadsUpNotificationSpace(
-            viewModel = viewModel.notifications,
-            isPeekFromBottom = true,
-            modifier = Modifier.padding(16.dp).fillMaxSize(),
+        // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
+        // transition from Shade scene.
+        Box(
+            modifier =
+                Modifier.element(Notifications.Elements.NotificationScrim)
+                    .fillMaxWidth()
+                    .height(0.dp)
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                        alpha = 1f
+                    }
+                    .background(MaterialTheme.colorScheme.surface)
+                    .align(Alignment.BottomCenter)
+                    .onPlaced { coordinates: LayoutCoordinates ->
+                        viewModel.notifications.onContentTopChanged(
+                            coordinates.positionInWindow().y
+                        )
+                        val boundsInWindow = coordinates.boundsInWindow()
+                        viewModel.notifications.onBoundsChanged(
+                            left = boundsInWindow.left,
+                            top = boundsInWindow.top,
+                            right = boundsInWindow.right,
+                            bottom = boundsInWindow.bottom,
+                        )
+                    }
         )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 747faab..770d654 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -18,13 +18,10 @@
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.Edge
@@ -66,12 +63,6 @@
     override fun SceneScope.Content(
         modifier: Modifier,
     ) {
-        Box(modifier = modifier) {
-            Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
-            HeadsUpNotificationSpace(
-                viewModel = notificationsViewModel,
-                modifier = Modifier.padding(16.dp).fillMaxSize(),
-            )
-        }
+        Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
index 5336bf6..0c66701 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
@@ -12,5 +12,5 @@
 
 // TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout.
 fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
-    return SceneTransitionSceneKey(name = toString(), identity = this)
+    return SceneTransitionSceneKey(debugName = toString(), identity = this)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 1545372..497fe87 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -32,6 +32,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.dimensionResource
@@ -62,6 +63,7 @@
 import com.android.systemui.util.animation.MeasurementInput
 import javax.inject.Inject
 import javax.inject.Named
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -154,62 +156,104 @@
     mediaHost: MediaHost,
     modifier: Modifier = Modifier,
 ) {
-    val localDensity = LocalDensity.current
+    val density = LocalDensity.current
     val layoutWidth = remember { mutableStateOf(0) }
+    val maxNotifScrimTop = remember { mutableStateOf(0f) }
 
     Box(
         modifier =
             modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
     )
     Box {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() })
-        ) {
-            CollapsedShadeHeader(
-                viewModel = viewModel.shadeHeaderViewModel,
-                createTintedIconManager = createTintedIconManager,
-                createBatteryMeterViewController = createBatteryMeterViewController,
-                statusBarIconController = statusBarIconController,
-                modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
-            )
-            QuickSettings(
-                modifier = Modifier.height(130.dp),
-                viewModel.qsSceneAdapter,
-            )
-
-            if (viewModel.isMediaVisible()) {
-                val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded)
-                MediaCarousel(
-                    modifier =
-                        Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints
-                            ->
-                            val placeable = measurable.measure(constraints)
-
-                            // Notify controller to size the carousel for the current space
-                            mediaHost.measurementInput =
-                                MeasurementInput(placeable.width, placeable.height)
-                            mediaCarouselController.setSceneContainerSize(
-                                placeable.width,
-                                placeable.height
+        Layout(
+            contents =
+                listOf(
+                    {
+                        Column(
+                            horizontalAlignment = Alignment.CenterHorizontally,
+                            modifier =
+                                Modifier.fillMaxWidth()
+                                    .clickable(onClick = { viewModel.onContentClicked() })
+                        ) {
+                            CollapsedShadeHeader(
+                                viewModel = viewModel.shadeHeaderViewModel,
+                                createTintedIconManager = createTintedIconManager,
+                                createBatteryMeterViewController = createBatteryMeterViewController,
+                                statusBarIconController = statusBarIconController,
+                                modifier =
+                                    Modifier.padding(
+                                        horizontal = Shade.Dimensions.HorizontalPadding
+                                    )
+                            )
+                            QuickSettings(
+                                modifier = Modifier.height(130.dp),
+                                viewModel.qsSceneAdapter,
                             )
 
-                            layout(placeable.width, placeable.height) {
-                                placeable.placeRelative(0, 0)
-                            }
-                        },
-                    mediaHost = mediaHost,
-                    layoutWidth = layoutWidth.value,
-                    layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(),
-                    carouselController = mediaCarouselController,
-                )
-            }
+                            if (viewModel.isMediaVisible()) {
+                                val mediaHeight =
+                                    dimensionResource(R.dimen.qs_media_session_height_expanded)
+                                MediaCarousel(
+                                    modifier =
+                                        Modifier.height(mediaHeight).fillMaxWidth().layout {
+                                            measurable,
+                                            constraints ->
+                                            val placeable = measurable.measure(constraints)
 
-            Spacer(modifier = Modifier.height(16.dp))
-            NotificationScrollingStack(
-                viewModel = viewModel.notifications,
-                modifier = Modifier.fillMaxWidth().weight(1f),
-            )
+                                            // Notify controller to size the carousel for the
+                                            // current space
+                                            mediaHost.measurementInput =
+                                                MeasurementInput(placeable.width, placeable.height)
+                                            mediaCarouselController.setSceneContainerSize(
+                                                placeable.width,
+                                                placeable.height
+                                            )
+
+                                            layout(placeable.width, placeable.height) {
+                                                placeable.placeRelative(0, 0)
+                                            }
+                                        },
+                                    mediaHost = mediaHost,
+                                    layoutWidth = layoutWidth.value,
+                                    layoutHeight = with(density) { mediaHeight.toPx() }.toInt(),
+                                    carouselController = mediaCarouselController,
+                                )
+                            }
+
+                            Spacer(modifier = Modifier.height(16.dp))
+                        }
+                    },
+                    {
+                        NotificationScrollingStack(
+                            viewModel = viewModel.notifications,
+                            maxScrimTop = { maxNotifScrimTop.value },
+                        )
+                    },
+                )
+        ) { measurables, constraints ->
+            check(measurables.size == 2)
+            check(measurables[0].size == 1)
+            check(measurables[1].size == 1)
+
+            val quickSettingsPlaceable = measurables[0][0].measure(constraints)
+
+            val notificationsMeasurable = measurables[1][0]
+            val notificationsScrimMaxHeight =
+                constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
+            val notificationsPlaceable =
+                notificationsMeasurable.measure(
+                    constraints.copy(
+                        minHeight = notificationsScrimMaxHeight,
+                        maxHeight = notificationsScrimMaxHeight
+                    )
+                )
+
+            maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                quickSettingsPlaceable.placeRelative(x = 0, y = 0)
+                notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
+            }
         }
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 7d3b0fb..b9b472f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -30,6 +30,7 @@
 internal fun CoroutineScope.animateToScene(
     layoutState: BaseSceneTransitionLayoutState,
     target: SceneKey,
+    transitionKey: TransitionKey?,
 ): TransitionState.Transition? {
     val transitionState = layoutState.transitionState
     if (transitionState.currentScene == target) {
@@ -45,7 +46,7 @@
     }
 
     return when (transitionState) {
-        is TransitionState.Idle -> animate(layoutState, target)
+        is TransitionState.Idle -> animate(layoutState, target, transitionKey)
         is TransitionState.Transition -> {
             // A transition is currently running: first check whether `transition.toScene` or
             // `transition.fromScene` is the same as our target scene, in which case the transition
@@ -61,13 +62,13 @@
                     // The transition is already finished (progress ~= 1): no need to animate. We
                     // finish the current transition early to make sure that the current state
                     // change is committed.
-                    layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    layoutState.finishTransition(transitionState, target)
                     null
                 } else {
                     // The transition is in progress: start the canned animation at the same
                     // progress as it was in.
                     // TODO(b/290184746): Also take the current velocity into account.
-                    animate(layoutState, target, startProgress = progress)
+                    animate(layoutState, target, transitionKey, startProgress = progress)
                 }
             } else if (transitionState.fromScene == target) {
                 // There is a transition from [target] to another scene: simply animate the same
@@ -78,16 +79,22 @@
                 if (progress.absoluteValue < ProgressVisibilityThreshold) {
                     // The transition is at progress ~= 0: no need to animate.We finish the current
                     // transition early to make sure that the current state change is committed.
-                    layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    layoutState.finishTransition(transitionState, target)
                     null
                 } else {
                     // TODO(b/290184746): Also take the current velocity into account.
-                    animate(layoutState, target, startProgress = progress, reversed = true)
+                    animate(
+                        layoutState,
+                        target,
+                        transitionKey,
+                        startProgress = progress,
+                        reversed = true,
+                    )
                 }
             } else {
                 // Generic interruption; the current transition is neither from or to [target].
                 // TODO(b/290930950): Better handle interruptions here.
-                animate(layoutState, target)
+                animate(layoutState, target, transitionKey)
             }
         }
     }
@@ -96,6 +103,7 @@
 private fun CoroutineScope.animate(
     layoutState: BaseSceneTransitionLayoutState,
     target: SceneKey,
+    transitionKey: TransitionKey?,
     startProgress: Float = 0f,
     reversed: Boolean = false,
 ): TransitionState.Transition {
@@ -127,7 +135,7 @@
     // Change the current layout state to start this new transition. This will compute the
     // TransformationSpec associated to this transition, which we need to initialize the Animatable
     // that will actually animate it.
-    layoutState.startTransition(transition)
+    layoutState.startTransition(transition, transitionKey)
 
     // The transformation now contains the spec that we should use to instantiate the Animatable.
     val animationSpec = layoutState.transformationSpec.progressSpec
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 9d4b69c..2596d4a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -42,19 +42,20 @@
 
 /** Key for a scene. */
 class SceneKey(
-    name: String,
+    debugName: String,
     identity: Any = Object(),
-) : Key(name, identity), UserActionResult {
+) : Key(debugName, identity), UserActionResult {
     @VisibleForTesting
     // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
     // access internal members.
-    val testTag: String = "scene:$name"
+    val testTag: String = "scene:$debugName"
 
     /** The unique [ElementKey] identifying this scene's root element. */
-    val rootElementKey = ElementKey(name, identity)
+    val rootElementKey = ElementKey(debugName, identity)
 
     // Implementation of [UserActionResult].
     override val toScene: SceneKey = this
+    override val transitionKey: TransitionKey? = null
     override val distance: UserActionDistance? = null
 
     override fun toString(): String {
@@ -64,7 +65,7 @@
 
 /** Key for an element. */
 class ElementKey(
-    name: String,
+    debugName: String,
     identity: Any = Object(),
 
     /**
@@ -72,11 +73,11 @@
      * or compose MovableElements.
      */
     val scenePicker: ElementScenePicker = DefaultElementScenePicker,
-) : Key(name, identity), ElementMatcher {
+) : Key(debugName, identity), ElementMatcher {
     @VisibleForTesting
     // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
     // access internal members.
-    val testTag: String = "element:$name"
+    val testTag: String = "element:$debugName"
 
     override fun matches(key: ElementKey, scene: SceneKey): Boolean {
         return key == this
@@ -99,8 +100,18 @@
 }
 
 /** Key for a shared value of an element. */
-class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+class ValueKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) {
     override fun toString(): String {
         return "ValueKey(debugName=$debugName)"
     }
 }
+
+/**
+ * Key for a transition. This can be used to specify which transition spec should be used when
+ * starting the transition between two scenes.
+ */
+class TransitionKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) {
+    override fun toString(): String {
+        return "TransitionKey(debugName=$debugName)"
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 5f615fd..529fc03 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -263,10 +263,10 @@
                     val deltaOffset = drag.position - initialDown.position
                     val delta =
                         when (orientation) {
-                            Orientation.Horizontal -> deltaOffset.y
+                            Orientation.Horizontal -> deltaOffset.x
                             Orientation.Vertical -> deltaOffset.y
                         }
-                    check(delta != 0f)
+                    check(delta != 0f) { "delta is equal to 0" }
                     overSlop = delta.sign
                 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 58c3be24..23af5ac 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -54,7 +54,7 @@
 
     private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
         if (isDrivingTransition || force) {
-            layoutState.startTransition(newTransition)
+            layoutState.startTransition(newTransition, newTransition.key)
 
             // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
             // layoutState.startTransition() is called, because it computes the
@@ -146,9 +146,10 @@
         val fromScene = layoutImpl.scene(transitionState.currentScene)
         updateSwipes(fromScene, startedPosition, pointersDown)
 
-        val (targetScene, distance) =
-            findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return
-        updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true)
+        val result =
+            findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
+                ?: return
+        updateTransition(SwipeTransition(fromScene, result), force = true)
     }
 
     private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
@@ -224,8 +225,8 @@
             computeFromSceneConsideringAcceleratedSwipe(swipeTransition)
 
         val isNewFromScene = fromScene.key != swipeTransition.fromScene
-        val (targetScene, distance) =
-            findTargetSceneAndDistance(
+        val result =
+            findUserActionResult(
                 fromScene,
                 swipeTransition.dragOffset,
                 updateSwipesResults = isNewFromScene,
@@ -236,9 +237,13 @@
                 }
         swipeTransition.dragOffset += acceleratedOffset
 
-        if (isNewFromScene || targetScene.key != swipeTransition.toScene) {
+        if (
+            isNewFromScene ||
+                result.toScene != swipeTransition.toScene ||
+                result.transitionKey != swipeTransition.key
+        ) {
             updateTransition(
-                SwipeTransition(fromScene, targetScene, distance).apply {
+                SwipeTransition(fromScene, result).apply {
                     this.dragOffset = swipeTransition.dragOffset
                 }
             )
@@ -306,7 +311,7 @@
     }
 
     /**
-     * Returns the target scene and distance from [fromScene] in the direction [directionOffset].
+     * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
      *
      * @param fromScene the scene from which we look for the target
      * @param directionOffset signed float that indicates the direction. Positive is down or right
@@ -322,60 +327,45 @@
      *   [directionOffset] is 0f and both direction are available, it will default to
      *   [upOrLeftResult].
      */
-    private inline fun findTargetSceneAndDistance(
+    private fun findUserActionResult(
         fromScene: Scene,
         directionOffset: Float,
         updateSwipesResults: Boolean,
-    ): Pair<Scene, Float>? {
+    ): UserActionResult? {
         if (updateSwipesResults) updateSwipesResults(fromScene)
 
-        // Compute the target scene depending on the current offset.
         return when {
             upOrLeftResult == null && downOrRightResult == null -> null
             (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
-                upOrLeftResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        -fromScene.getAbsoluteDistance(result.distance)
-                    )
-                }
-            else ->
-                downOrRightResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        fromScene.getAbsoluteDistance(result.distance)
-                    )
-                }
+                upOrLeftResult
+            else -> downOrRightResult
         }
     }
 
     /**
-     * A strict version of [findTargetSceneAndDistance] that will return null when there is no Scene
-     * in [directionOffset] direction
+     * A strict version of [findUserActionResult] that will return null when there is no Scene in
+     * [directionOffset] direction
      */
-    private inline fun findTargetSceneAndDistanceStrict(
-        fromScene: Scene,
-        directionOffset: Float,
-    ): Pair<Scene, Float>? {
+    private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
         return when {
-            directionOffset > 0f ->
-                upOrLeftResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        -fromScene.getAbsoluteDistance(result.distance),
-                    )
-                }
-            directionOffset < 0f ->
-                downOrRightResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        fromScene.getAbsoluteDistance(result.distance),
-                    )
-                }
+            directionOffset > 0f -> upOrLeftResult
+            directionOffset < 0f -> downOrRightResult
             else -> null
         }
     }
 
+    private fun computeAbsoluteDistance(
+        fromScene: Scene,
+        result: UserActionResult,
+    ): Float {
+        return if (result == upOrLeftResult) {
+            -fromScene.getAbsoluteDistance(result.distance)
+        } else {
+            check(result == downOrRightResult)
+            fromScene.getAbsoluteDistance(result.distance)
+        }
+    }
+
     internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
         // The state was changed since the drag started; don't do anything.
         if (!isDrivingTransition) {
@@ -440,8 +430,8 @@
 
             if (startFromIdlePosition) {
                 // If there is a target scene, we start the overscroll animation.
-                val (targetScene, distance) =
-                    findTargetSceneAndDistanceStrict(fromScene, velocity)
+                val result =
+                    findUserActionResultStrict(velocity)
                         ?: run {
                             // We will not animate
                             layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
@@ -449,7 +439,7 @@
                         }
 
                 updateTransition(
-                    SwipeTransition(fromScene, targetScene, distance).apply {
+                    SwipeTransition(fromScene, result).apply {
                         _currentScene = swipeTransition._currentScene
                     }
                 )
@@ -496,7 +486,17 @@
         }
     }
 
+    private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
+        return SwipeTransition(
+            result.transitionKey,
+            fromScene,
+            layoutImpl.scene(result.toScene),
+            computeAbsoluteDistance(fromScene, result),
+        )
+    }
+
     internal class SwipeTransition(
+        val key: TransitionKey?,
         val _fromScene: Scene,
         val _toScene: Scene,
         /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 7e0aa9c3..5258078 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -388,8 +388,9 @@
 /**
  * The result of performing a [UserAction].
  *
- * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to
- * easily create a [UserActionResult] with a fixed distance:
+ * Note: [UserActionResult] is implemented by [SceneKey], so you can also use scene keys directly
+ * when defining your [UserActionResult]s.
+ *
  * ```
  * SceneTransitionLayout(...) {
  *     scene(
@@ -397,7 +398,7 @@
  *         userActions =
  *             mapOf(
  *                 Swipe.Right to Scene.Bar,
- *                 Swipe.Down to Scene.Doe withDistance 100.dp,
+ *                 Swipe.Down to Scene.Doe,
  *             )
  *         )
  *     ) { ... }
@@ -408,6 +409,9 @@
     /** The scene we should be transitioning to during the [UserAction]. */
     val toScene: SceneKey
 
+    /** The key of the transition that should be used. */
+    val transitionKey: TransitionKey?
+
     /**
      * The distance the action takes to animate from 0% to 100%.
      *
@@ -416,6 +420,32 @@
     val distance: UserActionDistance?
 }
 
+/** Create a [UserActionResult] to [toScene] with the given [distance] and [transitionKey]. */
+fun UserActionResult(
+    toScene: SceneKey,
+    distance: UserActionDistance? = null,
+    transitionKey: TransitionKey? = null,
+): UserActionResult {
+    return object : UserActionResult {
+        override val toScene: SceneKey = toScene
+        override val transitionKey: TransitionKey? = transitionKey
+        override val distance: UserActionDistance? = distance
+    }
+}
+
+/** Create a [UserActionResult] to [toScene] with the given fixed [distance] and [transitionKey]. */
+fun UserActionResult(
+    toScene: SceneKey,
+    distance: Dp,
+    transitionKey: TransitionKey? = null,
+): UserActionResult {
+    return UserActionResult(
+        toScene = toScene,
+        distance = FixedDistance(distance),
+        transitionKey = transitionKey,
+    )
+}
+
 interface UserActionDistance {
     /**
      * Return the **absolute** distance of the user action given the size of the scene we are
@@ -424,22 +454,6 @@
     fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
 }
 
-/**
- * A utility function to make it possible to define user actions with a distance using the syntax
- * `Swipe.Up to Scene.foo withDistance 100.dp`
- */
-infix fun Pair<UserAction, SceneKey>.withDistance(
-    distance: Dp
-): Pair<UserAction, UserActionResult> {
-    val scene = second
-    val distance = FixedDistance(distance)
-    return first to
-        object : UserActionResult {
-            override val toScene: SceneKey = scene
-            override val distance: UserActionDistance = distance
-        }
-}
-
 /** The user action has a fixed [absoluteDistance]. */
 private class FixedDistance(private val distance: Dp) : UserActionDistance {
     override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 956e326..aee6f9e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -92,6 +92,7 @@
     fun setTargetScene(
         targetScene: SceneKey,
         coroutineScope: CoroutineScope,
+        transitionKey: TransitionKey? = null,
     ): TransitionState.Transition?
 }
 
@@ -213,11 +214,14 @@
     }
 
     /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */
-    internal fun startTransition(transition: TransitionState.Transition) {
+    internal fun startTransition(
+        transition: TransitionState.Transition,
+        transitionKey: TransitionKey?,
+    ) {
         // Compute the [TransformationSpec] when the transition starts.
         transformationSpec =
             transitions
-                .transitionSpec(transition.fromScene, transition.toScene)
+                .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
                 .transformationSpec()
 
         transitionState = transition
@@ -265,7 +269,11 @@
                 // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
                 // late.
                 val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey
-                animateToScene(layoutState = this@HoistedSceneTransitionLayoutScene, newKey)
+                animateToScene(
+                    layoutState = this@HoistedSceneTransitionLayoutScene,
+                    target = newKey,
+                    transitionKey = null,
+                )
             }
         }
     }
@@ -278,14 +286,14 @@
 ) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) {
     override fun setTargetScene(
         targetScene: SceneKey,
-        coroutineScope: CoroutineScope
+        coroutineScope: CoroutineScope,
+        transitionKey: TransitionKey?,
     ): TransitionState.Transition? {
-        return with(this) {
-            coroutineScope.animateToScene(
-                layoutState = this@MutableSceneTransitionLayoutStateImpl,
-                target = targetScene,
-            )
-        }
+        return coroutineScope.animateToScene(
+            layoutState = this@MutableSceneTransitionLayoutStateImpl,
+            target = targetScene,
+            transitionKey = transitionKey,
+        )
     }
 
     override fun CoroutineScope.onChangeScene(scene: SceneKey) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index ac423b7..b8f9359 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -42,32 +42,42 @@
     internal val defaultSwipeSpec: SpringSpec<Float>,
     internal val transitionSpecs: List<TransitionSpecImpl>,
 ) {
-    private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpecImpl>>()
+    private val cache =
+        mutableMapOf<
+            SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>>
+        >()
 
-    internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpecImpl {
-        return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
+    internal fun transitionSpec(
+        from: SceneKey,
+        to: SceneKey,
+        key: TransitionKey?,
+    ): TransitionSpecImpl {
+        return cache
+            .getOrPut(from) { mutableMapOf() }
+            .getOrPut(to) { mutableMapOf() }
+            .getOrPut(key) { findSpec(from, to, key) }
     }
 
-    private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpecImpl {
-        val spec = transition(from, to) { it.from == from && it.to == to }
+    private fun findSpec(from: SceneKey, to: SceneKey, key: TransitionKey?): TransitionSpecImpl {
+        val spec = transition(from, to, key) { it.from == from && it.to == to }
         if (spec != null) {
             return spec
         }
 
-        val reversed = transition(from, to) { it.from == to && it.to == from }
+        val reversed = transition(from, to, key) { it.from == to && it.to == from }
         if (reversed != null) {
             return reversed.reversed()
         }
 
         val relaxedSpec =
-            transition(from, to) {
+            transition(from, to, key) {
                 (it.from == from && it.to == null) || (it.to == to && it.from == null)
             }
         if (relaxedSpec != null) {
             return relaxedSpec
         }
 
-        return transition(from, to) {
+        return transition(from, to, key) {
                 (it.from == to && it.to == null) || (it.to == from && it.from == null)
             }
             ?.reversed()
@@ -77,11 +87,12 @@
     private fun transition(
         from: SceneKey,
         to: SceneKey,
+        key: TransitionKey?,
         filter: (TransitionSpecImpl) -> Boolean,
     ): TransitionSpecImpl? {
         var match: TransitionSpecImpl? = null
         transitionSpecs.fastForEach { spec ->
-            if (filter(spec)) {
+            if (spec.key == key && filter(spec)) {
                 if (match != null) {
                     error("Found multiple transition specs for transition $from => $to")
                 }
@@ -92,7 +103,7 @@
     }
 
     private fun defaultTransition(from: SceneKey, to: SceneKey) =
-        TransitionSpecImpl(from, to, TransformationSpec.EmptyProvider)
+        TransitionSpecImpl(key = null, from, to, TransformationSpec.EmptyProvider)
 
     companion object {
         internal val DefaultSwipeSpec =
@@ -107,6 +118,9 @@
 
 /** The definition of a transition between [from] and [to]. */
 interface TransitionSpec {
+    /** The key of this [TransitionSpec]. */
+    val key: TransitionKey?
+
     /**
      * The scene we are transitioning from. If `null`, this spec can be used to animate from any
      * scene.
@@ -164,12 +178,14 @@
 }
 
 internal class TransitionSpecImpl(
+    override val key: TransitionKey?,
     override val from: SceneKey?,
     override val to: SceneKey?,
     private val transformationSpec: () -> TransformationSpecImpl,
 ) : TransitionSpec {
     override fun reversed(): TransitionSpecImpl {
         return TransitionSpecImpl(
+            key = key,
             from = to,
             to = from,
             transformationSpec = {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 04e0937..d93911d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -42,10 +42,14 @@
      * any scene. For the animation specification to apply only when transitioning between two
      * specific scenes, use [from] instead.
      *
+     * If [key] is not `null`, then this transition will only be used if the same key is specified
+     * when triggering the transition.
+     *
      * @see from
      */
     fun to(
         to: SceneKey,
+        key: TransitionKey? = null,
         builder: TransitionBuilder.() -> Unit = {},
     ): TransitionSpec
 
@@ -55,7 +59,8 @@
      * the destination scene via the [to] argument.
      *
      * When looking up which transition should be used when animating from scene A to scene B, we
-     * pick the single transition matching one of these predicates (in order of importance):
+     * pick the single transition with the given [key] and matching one of these predicates (in
+     * order of importance):
      * 1. from == A && to == B
      * 2. to == A && from == B, which is then treated in reverse.
      * 3. (from == A && to == null) || (from == null && to == B)
@@ -64,6 +69,7 @@
     fun from(
         from: SceneKey,
         to: SceneKey? = null,
+        key: TransitionKey? = null,
         builder: TransitionBuilder.() -> Unit = {},
     ): TransitionSpec
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index df186a1..9b16d46 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -49,21 +49,27 @@
 
     val transitionSpecs = mutableListOf<TransitionSpecImpl>()
 
-    override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
-        return transition(from = null, to = to, builder)
+    override fun to(
+        to: SceneKey,
+        key: TransitionKey?,
+        builder: TransitionBuilder.() -> Unit
+    ): TransitionSpec {
+        return transition(from = null, to = to, key = key, builder)
     }
 
     override fun from(
         from: SceneKey,
         to: SceneKey?,
+        key: TransitionKey?,
         builder: TransitionBuilder.() -> Unit
     ): TransitionSpec {
-        return transition(from = from, to = to, builder)
+        return transition(from = from, to = to, key = key, builder)
     }
 
     private fun transition(
         from: SceneKey?,
         to: SceneKey?,
+        key: TransitionKey?,
         builder: TransitionBuilder.() -> Unit,
     ): TransitionSpec {
         fun transformationSpec(): TransformationSpecImpl {
@@ -75,7 +81,7 @@
             )
         }
 
-        val spec = TransitionSpecImpl(from, to, ::transformationSpec)
+        val spec = TransitionSpecImpl(key, from, to, ::transformationSpec)
         transitionSpecs.add(spec)
         return spec
     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 48825fb..c61917d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -45,7 +45,10 @@
     @Test
     fun isTransitioningTo_transition() {
         val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
-        state.startTransition(transition(from = TestScenes.SceneA, to = TestScenes.SceneB))
+        state.startTransition(
+            transition(from = TestScenes.SceneA, to = TestScenes.SceneB),
+            transitionKey = null
+        )
 
         assertThat(state.isTransitioning()).isTrue()
         assertThat(state.isTransitioning(from = TestScenes.SceneA)).isTrue()
@@ -91,6 +94,16 @@
     }
 
     @Test
+    fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+
+        // Progress is 0f, so we don't animate at all and directly snap back to A.
+        assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA))
+    }
+
+    @Test
     fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest {
         val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
 
@@ -106,4 +119,43 @@
         testScheduler.advanceUntilIdle()
         assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
     }
+
+    @Test
+    fun setTargetScene_withTransitionKey() = runMonotonicClockTest {
+        val transitionkey = TransitionKey(debugName = "foo")
+        val state =
+            MutableSceneTransitionLayoutState(
+                TestScenes.SceneA,
+                transitions =
+                    transitions {
+                        from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
+                        from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) {
+                            fade(TestElements.Foo)
+                            fade(TestElements.Bar)
+                        }
+                    },
+            )
+                as MutableSceneTransitionLayoutStateImpl
+
+        // Default transition from A to B.
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+        assertThat(state.transformationSpec.transformations).hasSize(1)
+
+        // Go back to A.
+        state.setTargetScene(TestScenes.SceneA, coroutineScope = this)
+        testScheduler.advanceUntilIdle()
+        assertThat(state.currentTransition).isNull()
+        assertThat(state.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Specific transition from A to B.
+        assertThat(
+                state.setTargetScene(
+                    TestScenes.SceneB,
+                    coroutineScope = this,
+                    transitionKey = transitionkey,
+                )
+            )
+            .isNotNull()
+        assertThat(state.transformationSpec.transformations).hasSize(2)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index d81631a..543ed04 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -384,7 +384,13 @@
                 scene(
                     TestScenes.SceneA,
                     userActions =
-                        mapOf(Swipe.Down to TestScenes.SceneB withDistance verticalSwipeDistance),
+                        mapOf(
+                            Swipe.Down to
+                                UserActionResult(
+                                    toScene = TestScenes.SceneB,
+                                    distance = verticalSwipeDistance,
+                                )
+                        ),
                 ) {
                     Spacer(Modifier.fillMaxSize())
                 }
@@ -442,6 +448,23 @@
         transition = layoutState.currentTransition
         assertThat(transition).isNotNull()
         assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+
+        // Release the finger, animating back to scene A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(layoutState.currentTransition).isNull()
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Swipe left by exactly touchSlop, so that the drag overSlop is 0f.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // We should still correctly compute that we are swiping down to scene B.
+        transition = layoutState.currentTransition
+        assertThat(transition).isNotNull()
+        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
     }
 
     @Test
@@ -475,4 +498,54 @@
         }
         assertThat(layoutState.currentTransition).isNotNull()
     }
+
+    @Test
+    fun transitionKey() {
+        val transitionkey = TransitionKey(debugName = "foo")
+        val state =
+            MutableSceneTransitionLayoutState(
+                TestScenes.SceneA,
+                transitions {
+                    from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
+                    from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) {
+                        fade(TestElements.Foo)
+                        fade(TestElements.Bar)
+                    }
+                }
+            )
+                as MutableSceneTransitionLayoutStateImpl
+
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state, Modifier.size(LayoutWidth, LayoutHeight)) {
+                scene(
+                    TestScenes.SceneA,
+                    userActions =
+                        mapOf(
+                            Swipe.Down to TestScenes.SceneB,
+                            Swipe.Up to
+                                UserActionResult(TestScenes.SceneB, transitionKey = transitionkey)
+                        )
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+                scene(TestScenes.SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        // Swipe down for the default transition from A to B.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+
+        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+        assertThat(state.transformationSpec.transformations).hasSize(1)
+
+        // Move the pointer up to swipe to scene B using the new transition.
+        rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
+        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+        assertThat(state.transformationSpec.transformations).hasSize(2)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index 140baca..1beafcc 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -177,7 +177,7 @@
         // to B we defined.
         val transformations =
             transitions
-                .transitionSpec(from = TestScenes.SceneB, to = TestScenes.SceneA)
+                .transitionSpec(from = TestScenes.SceneB, to = TestScenes.SceneA, key = null)
                 .transformationSpec()
                 .transformations
 
@@ -207,7 +207,7 @@
         // A => B does not have a custom spec.
         assertThat(
                 transitions
-                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneB)
+                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneB, key = null)
                     .transformationSpec()
                     .swipeSpec
             )
@@ -216,7 +216,7 @@
         // A => C has a custom swipe spec.
         assertThat(
                 transitions
-                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneC)
+                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneC, key = null)
                     .transformationSpec()
                     .swipeSpec
             )
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 6d6d575..2d71a6e 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -89,7 +89,7 @@
             SceneTransitionLayout(
                 currentScene,
                 onChangeScene,
-                transitions { from(fromScene, to = toScene, transition) },
+                transitions { from(fromScene, to = toScene, builder = transition) },
                 layoutModifier,
             ) {
                 scene(fromScene, content = fromSceneContent)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
index e050604..34c4dfb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
@@ -33,6 +33,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.withContext
@@ -518,6 +519,7 @@
                 awaitClose { context.contentResolver.unregisterContentObserver(observer) }
             }
             .onStart { emit(Unit) }
+            .flowOn(backgroundDispatcher)
     }
 
     private fun String.toIntent(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
index be6bb9c..107293e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
@@ -24,6 +24,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class SysuiTestCaseSelfTest : SysuiTestCase() {
     private val contextBeforeSetup = context
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
index 9287edf..c2f0c6f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index c86c747..b6605ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class ColorCorrectionRepositoryImplTest : SysuiTestCase() {
 
     private val testUser1 = UserHandle.of(1)!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 4853529..30eb782 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class ColorInversionRepositoryImplTest : SysuiTestCase() {
 
     private val testUser1 = UserHandle.of(1)!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
index ce22e28..ed3b4c0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() {
     private val secureSettings = FakeSettings()
     private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
index 39f0d57..0596205 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FaceHelpMessageDeferralTest : SysuiTestCase() {
     val threshold = .75f
     @Mock lateinit var logger: BiometricMessageDeferralLogger
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
index a67b093..d317aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class EmergencyServicesRepositoryImplTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
index 4aea4f3..db83b3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -29,6 +29,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
     private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
     @Mock
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index c193d14..fbb5415 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.content.pm.UserInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -26,18 +27,27 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.model.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.google.common.truth.Truth.assertThat
+import java.util.UUID
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,19 +61,22 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val authenticationInteractor = kosmos.authenticationInteractor
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
+    private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
     private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private val isInputEnabled = MutableStateFlow(true)
 
-    private val underTest by lazy {
+    private val underTest =
         PasswordBouncerViewModel(
             viewModelScope = testScope.backgroundScope,
+            isInputEnabled = isInputEnabled.asStateFlow(),
             interactor = bouncerInteractor,
-            isInputEnabled.asStateFlow(),
+            inputMethodInteractor = inputMethodInteractor,
+            selectedUserInteractor = selectedUserInteractor,
         )
-    }
 
     @Before
     fun setUp() {
@@ -270,6 +283,52 @@
             assertThat(isTextFieldFocusRequested).isTrue()
         }
 
+    @Test
+    fun isImeSwitcherButtonVisible() =
+        testScope.runTest {
+            val selectedUserId by collectLastValue(selectedUserInteractor.selectedUser)
+            selectUser(USER_INFOS.first())
+
+            enableInputMethodsForUser(checkNotNull(selectedUserId))
+
+            // Assert initial value, before the UI subscribes.
+            assertThat(underTest.isImeSwitcherButtonVisible.value).isFalse()
+
+            // Subscription starts; verify a fresh value is fetched.
+            val isImeSwitcherButtonVisible by collectLastValue(underTest.isImeSwitcherButtonVisible)
+            assertThat(isImeSwitcherButtonVisible).isTrue()
+
+            // Change the user, verify a fresh value is fetched.
+            selectUser(USER_INFOS.last())
+
+            assertThat(
+                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(
+                        checkNotNull(selectedUserId)
+                    )
+                )
+                .isFalse()
+            assertThat(isImeSwitcherButtonVisible).isFalse()
+
+            // Enable IMEs and add another subscriber; verify a fresh value is fetched.
+            enableInputMethodsForUser(checkNotNull(selectedUserId))
+            val collector2 by collectLastValue(underTest.isImeSwitcherButtonVisible)
+            assertThat(collector2).isTrue()
+        }
+
+    @Test
+    fun onImeSwitcherButtonClicked() =
+        testScope.runTest {
+            val displayId = 7
+            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isNotEqualTo(displayId)
+
+            underTest.onImeSwitcherButtonClicked(displayId)
+            runCurrent()
+
+            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isEqualTo(displayId)
+        }
+
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.desiredScene)
         val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
@@ -310,8 +369,45 @@
         runCurrent()
     }
 
+    private fun TestScope.selectUser(userInfo: UserInfo) {
+        kosmos.fakeUserRepository.selectedUser.value =
+            SelectedUserModel(
+                userInfo = userInfo,
+                selectionStatus = SelectionStatus.SELECTION_COMPLETE
+            )
+        advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES)
+    }
+
+    private suspend fun enableInputMethodsForUser(userId: Int) {
+        kosmos.fakeInputMethodRepository.setEnabledInputMethods(
+            userId,
+            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+        )
+        assertThat(inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue()
+    }
+
+    private fun createInputMethodWithSubtypes(
+        auxiliarySubtypes: Int,
+        nonAuxiliarySubtypes: Int,
+    ): InputMethodModel {
+        return InputMethodModel(
+            imeId = UUID.randomUUID().toString(),
+            subtypes =
+                List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+                    InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+                }
+        )
+    }
+
     companion object {
         private const val ENTER_YOUR_PASSWORD = "Enter your password"
         private const val WRONG_PASSWORD = "Wrong password"
+
+        private val USER_INFOS =
+            listOf(
+                UserInfo(100, "First user", 0),
+                UserInfo(101, "Second user", 0),
+            )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
index 55016bb..25a287c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -24,6 +24,7 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class PinInputViewModelTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
new file mode 100644
index 0000000..a8fe16b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2024 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.communal
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.dockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSceneStartableTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: CommunalSceneStartable
+
+    @Before
+    fun setUp() =
+        with(kosmos) {
+            underTest =
+                CommunalSceneStartable(
+                        dockManager = dockManager,
+                        communalInteractor = communalInteractor,
+                        keyguardTransitionInteractor = keyguardTransitionInteractor,
+                        applicationScope = applicationCoroutineScope,
+                        bgScope = applicationCoroutineScope,
+                    )
+                    .apply { start() }
+        }
+
+    @Test
+    fun keyguardGoesAway_forceBlankScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDreaming_forceBlankScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDocked_forceCommunalScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                updateDocked(true)
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                updateDocked(true)
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceAsleep_forceBlankSceneAfterTimeout() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OFF,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OFF,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.OFF,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    testScope = this
+                )
+
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+            }
+        }
+
+    @Test
+    fun dockingOnLockscreen_forcesCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                // device is docked while on the lockscreen
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                updateDocked(true)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+            }
+        }
+
+    @Test
+    fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
+        with(kosmos) {
+            testScope.runTest {
+                communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                // device is docked while on the lockscreen
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                updateDocked(true)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY / 2)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                // dream starts shortly after docking
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    private fun TestScope.updateDocked(docked: Boolean) =
+        with(kosmos) {
+            runCurrent()
+            fakeDockManager.setIsDocked(docked)
+            fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+            runCurrent()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 92b75cb..76b0d4a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -40,6 +40,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalMediaRepositoryImplTest : SysuiTestCase() {
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var mediaData: MediaData
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
index 820bfbf..d15e15e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
     private lateinit var underTest: CommunalPrefsRepositoryImpl
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
index c4a8582..0c66bbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
@@ -38,6 +38,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index ee01bf9..c5485c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -642,6 +642,53 @@
         }
 
     @Test
+    fun isCommunalVisible() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(CommunalSceneKey.Blank)
+                )
+            communalRepository.setTransitionState(transitionState)
+
+            // isCommunalVisible is false when not on communal.
+            val isCommunalVisible by collectLastValue(underTest.isCommunalVisible)
+            assertThat(isCommunalVisible).isEqualTo(false)
+
+            // Start transition to communal.
+            transitionState.value =
+                ObservableCommunalTransitionState.Transition(
+                    fromScene = CommunalSceneKey.Blank,
+                    toScene = CommunalSceneKey.Communal,
+                    progress = flowOf(0f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+
+            // isCommunalVisible is true once transition starts.
+            assertThat(isCommunalVisible).isEqualTo(true)
+
+            // Finish transition to communal
+            transitionState.value =
+                ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+
+            // isCommunalVisible is true since we're on communal.
+            assertThat(isCommunalVisible).isEqualTo(true)
+
+            // Start transition away from communal.
+            transitionState.value =
+                ObservableCommunalTransitionState.Transition(
+                    fromScene = CommunalSceneKey.Communal,
+                    toScene = CommunalSceneKey.Blank,
+                    progress = flowOf(1.0f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+
+            // isCommunalVisible is still true as the false as soon as transition away runs.
+            assertThat(isCommunalVisible).isEqualTo(true)
+        }
+
+    @Test
     fun testShowWidgetEditorStartsActivity() =
         testScope.runTest {
             underTest.showWidgetEditor()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 3aa99c4..89a4c04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -23,16 +23,27 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.widgets.CommunalAppWidgetHost
 import com.android.systemui.communal.widgets.CommunalAppWidgetHostView
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidJUnit4::class)
 class CommunalAppWidgetHostTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var underTest: CommunalAppWidgetHost
@@ -43,9 +54,11 @@
         underTest =
             CommunalAppWidgetHost(
                 context = context,
+                backgroundScope = kosmos.applicationCoroutineScope,
                 hostId = 116,
                 interactionHandler = mock(),
-                looper = testableLooper.looper
+                looper = testableLooper.looper,
+                logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"),
             )
     }
 
@@ -64,4 +77,23 @@
         assertThat(view).isNotNull()
         assertThat(view.appWidgetId).isEqualTo(appWidgetId)
     }
+
+    @Test
+    fun appWidgetIdToRemove_emit() =
+        testScope.runTest {
+            val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove)
+
+            // Nothing should be emitted yet
+            assertThat(appWidgetIdToRemove).isNull()
+
+            underTest.onAppWidgetRemoved(appWidgetId = 1)
+            runCurrent()
+
+            assertThat(appWidgetIdToRemove).isEqualTo(1)
+
+            underTest.onAppWidgetRemoved(appWidgetId = 2)
+            runCurrent()
+
+            assertThat(appWidgetIdToRemove).isEqualTo(2)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index a3654b6..032d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -21,14 +21,21 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -47,6 +54,8 @@
 
     @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
 
+    private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
+
     private lateinit var underTest: CommunalAppWidgetHostStartable
 
     @Before
@@ -54,6 +63,9 @@
         MockitoAnnotations.initMocks(this)
         kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
 
+        appWidgetIdToRemove = MutableSharedFlow()
+        whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove)
+
         underTest =
             CommunalAppWidgetHostStartable(
                 appWidgetHost,
@@ -120,6 +132,38 @@
             }
         }
 
+    @Test
+    fun removeAppWidgetReportedByHost() =
+        with(kosmos) {
+            testScope.runTest {
+                // Set up communal widgets
+                val widget1 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+                val widget2 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+                val widget3 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+                fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
+
+                underTest.start()
+
+                // Assert communal widgets has 3
+                val communalWidgets by
+                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
+                assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+
+                // Report app widget 1 to remove and assert widget removed
+                appWidgetIdToRemove.emit(1)
+                runCurrent()
+                assertThat(communalWidgets).containsExactly(widget2, widget3)
+
+                // Report app widget 3 to remove and assert widget removed
+                appWidgetIdToRemove.emit(3)
+                runCurrent()
+                assertThat(communalWidgets).containsExactly(widget2)
+            }
+        }
+
     private suspend fun setCommunalAvailable(available: Boolean) =
         with(kosmos) {
             fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index b54c5bd..9536084 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -32,6 +32,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceEntryRepositoryTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 32943a1..51db451 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -33,6 +33,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceUnlockedInteractorTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
new file mode 100644
index 0000000..e9e85c9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 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.deviceentry.domain.ui.binder
+
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEventListener
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.deviceentry.ui.binder.liftToRunFaceAuthBinder
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.sensors.asyncSensorManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class LiftToRunFaceAuthBinderTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val sensorManager = kosmos.asyncSensorManager
+    private val powerRepository = kosmos.fakePowerRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val bouncerRepository = kosmos.keyguardBouncerRepository
+    private val biometricSettingsRepository = kosmos.biometricSettingsRepository
+    private val packageManager = kosmos.packageManager
+
+    @Captor private lateinit var triggerEventListenerCaptor: ArgumentCaptor<TriggerEventListener>
+    @Mock private lateinit var mockSensor: Sensor
+
+    private val underTest = kosmos.liftToRunFaceAuthBinder
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
+        whenever(sensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)).thenReturn(mockSensor)
+    }
+
+    @Test
+    fun doNotListenForGesture() =
+        testScope.runTest {
+            start()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun awakeKeyguard_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun faceNotEnrolled_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            runCurrent()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun notInteractive_doNotListenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            powerRepository.setInteractive(false)
+            runCurrent()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun primaryBouncer_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(false)
+            givenPrimaryBouncerShowing()
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun alternateBouncer_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(false)
+            givenAlternateBouncerShowing()
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun restartListeningForGestureAfterSensorTrigger() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+            clearInvocations(sensorManager)
+
+            triggerEventListenerCaptor.value.onTrigger(null)
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun cancelTriggerSensor_keyguardNotAwakeAnymore() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+
+            givenAwakeKeyguard(false)
+            runCurrent()
+            verifyCancelTriggerSensor()
+        }
+
+    private fun start() {
+        underTest.start()
+        biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+        givenAwakeKeyguard(false)
+        givenBouncerNotShowing()
+    }
+
+    private fun givenAwakeKeyguard(isAwake: Boolean) {
+        powerRepository.setInteractive(isAwake)
+        keyguardRepository.setKeyguardShowing(isAwake)
+        keyguardRepository.setKeyguardOccluded(false)
+    }
+
+    private fun givenPrimaryBouncerShowing() {
+        bouncerRepository.setPrimaryShow(true)
+        bouncerRepository.setAlternateVisible(false)
+    }
+
+    private fun givenBouncerNotShowing() {
+        bouncerRepository.setPrimaryShow(false)
+        bouncerRepository.setAlternateVisible(false)
+    }
+
+    private fun givenAlternateBouncerShowing() {
+        bouncerRepository.setPrimaryShow(false)
+        bouncerRepository.setAlternateVisible(true)
+    }
+
+    private fun verifyRequestTriggerSensor() {
+        verify(sensorManager).requestTriggerSensor(capture(triggerEventListenerCaptor), any())
+    }
+
+    private fun verifyNeverRequestsTriggerSensor() {
+        verify(sensorManager, never()).requestTriggerSensor(any(), any())
+    }
+
+    private fun verifyCancelTriggerSensor() {
+        verify(sensorManager).cancelTriggerSensor(any(), any())
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
new file mode 100644
index 0000000..06275fa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.dock
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.dockManager: DockManager by Kosmos.Fixture { fakeDockManager }
+val Kosmos.fakeDockManager: DockManagerFake by Kosmos.Fixture { DockManagerFake() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
index 2c6c793..d9dcfdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DreamOverlayCallbackControllerTest : SysuiTestCase() {
 
     @Mock private lateinit var callback: DreamOverlayCallbackController.Callback
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
index d379dc6..5ae8595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayNotificationCountProviderTest extends SysuiTestCase {
     @Mock
     NotificationListener mNotificationListener;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 8bf878c..b46f2aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -50,6 +50,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayStateControllerTest extends SysuiTestCase {
     @Mock
     DreamOverlayStateController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
index 7ff345c..ad353ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
@@ -37,6 +37,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayStatusBarItemsProviderTest extends SysuiTestCase {
     @Mock
     DreamOverlayStatusBarItemsProvider.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
index e0c6ab2..cb5702ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
@@ -42,6 +42,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class AssistantAttentionConditionTest extends SysuiTestCase {
     @Mock
     Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
index 480754c..96d3c93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
@@ -45,6 +45,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamConditionTest extends SysuiTestCase {
     @Mock
     Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
new file mode 100644
index 0000000..efccf7a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.homeControlsComponentInteractor by
+    Kosmos.Fixture {
+        HomeControlsComponentInteractor(
+            selectedComponentRepository = selectedComponentRepository,
+            controlsComponent,
+            authorizedPanelsRepository = authorizedPanelsRepository,
+            userRepository = fakeUserRepository,
+            bgScope = applicationCoroutineScope,
+        )
+    }
+
+val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() }
+val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() }
+val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
new file mode 100644
index 0000000..ce74a90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.FakeSelectedComponentRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsComponentInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var underTest: HomeControlsComponentInteractor
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var selectedComponentRepository: FakeSelectedComponentRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        userRepository = kosmos.fakeUserRepository
+        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+
+        controlsComponent = kosmos.controlsComponent
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        controlsListingController = kosmos.controlsListingController
+        selectedComponentRepository = kosmos.selectedComponentRepository
+
+        selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle)
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        underTest =
+            HomeControlsComponentInteractor(
+                selectedComponentRepository,
+                controlsComponent,
+                authorizedPanelsRepository,
+                userRepository,
+                kosmos.applicationCoroutineScope,
+            )
+    }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForSelectedItemByUser() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                whenever(controlsListingController.getCurrentServices())
+                    .thenReturn(
+                        listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+                    )
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate(false)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForDifferentUsers() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(ANOTHER_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                whenever(controlsComponent.getControlsListingController())
+                    .thenReturn(Optional.empty())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
+        val listings =
+            listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean))
+        val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
+        callback.onServicesUpdated(listings)
+    }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+
+        private const val ANOTHER_USER_ID = 1
+        private val ANOTHER_USER =
+            UserInfo(
+                /* id= */ ANOTHER_USER_ID,
+                /* name= */ "another user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE = "pkg"
+        private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
new file mode 100644
index 0000000..d28b6bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+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.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamServiceTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+    @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
+    @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
+    @Mock private lateinit var activity: Activity
+    private val logBuffer: LogBuffer = create()
+
+    private lateinit var underTest: HomeControlsDreamService
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var fakeDreamActivityProvider: DreamActivityProvider
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
+            .thenReturn(taskFragmentComponent)
+
+        controlsSettingsRepository = FakeControlsSettingsRepository()
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        fakeDreamActivityProvider = DreamActivityProvider { activity }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+    }
+
+    @Test
+    fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
+    }
+
+    @Test
+    fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        underTest.onDetachedFromWindow()
+        verify(taskFragmentComponent).destroy()
+    }
+
+    @Test
+    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
+        fakeDreamActivityProvider = DreamActivityProvider { null }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+    }
+
+    companion object {
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
new file mode 100644
index 0000000..6610e70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    @Mock private lateinit var packageManager: PackageManager
+
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var selectedComponentRepository: SelectedComponentRepository
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    private lateinit var startable: HomeControlsDreamStartable
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        selectedComponentRepository = kosmos.selectedComponentRepository
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        userRepository = kosmos.fakeUserRepository
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        userRepository.setUserInfos(listOf(PRIMARY_USER))
+
+        whenever(authorizedPanelsRepository.getAuthorizedPanels())
+            .thenReturn(setOf(TEST_PACKAGE_PANEL))
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+        whenever(controlsListingController.getCurrentServices())
+            .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        startable =
+            HomeControlsDreamStartable(
+                mContext,
+                packageManager,
+                homeControlsComponentInteractor,
+                kosmos.applicationCoroutineScope
+            )
+    }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartEnablesHomeControlsDreamServiceWhenPanelComponentIsNotNull() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @DisableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDoesNotRunDreamServiceWhenFlagIsDisabled() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any())
+        }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
index 74c1970..2a9bc4a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
@@ -28,7 +28,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.shared.system.InputChannelCompat;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import org.junit.Before;
@@ -47,8 +46,6 @@
     @Mock
     CentralSurfaces mCentralSurfaces;
     @Mock
-    NotificationShadeWindowController mNotificationShadeWindowController;
-    @Mock
     DreamTouchHandler.TouchSession mTouchSession;
     CommunalTouchHandler mTouchHandler;
 
@@ -59,17 +56,10 @@
         MockitoAnnotations.initMocks(this);
         mTouchHandler = new CommunalTouchHandler(
                 Optional.of(mCentralSurfaces),
-                mNotificationShadeWindowController,
                 INITIATION_WIDTH);
     }
 
     @Test
-    public void testSessionStartForcesShadeOpen() {
-        mTouchHandler.onSessionStart(mTouchSession);
-        verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler);
-    }
-
-    @Test
     public void testEventPropagation() {
         final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
index 017fdbe..97052a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class BouncerlessScrimControllerTest extends SysuiTestCase {
     @Mock
     BouncerlessScrimController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
index 4ee4a60..ebbcf98 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class ScrimManagerTest extends SysuiTestCase {
     @Mock
     ScrimController mBouncerlessScrimController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
index 61b2057..db52a45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
@@ -28,6 +28,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FoldPostureTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
new file mode 100644
index 0000000..857cdce
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.data.repository
+
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import android.view.inputmethod.InputMethodSubtype
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodRepositoryTest : SysuiTestCase() {
+
+    @Mock private lateinit var inputMethodManager: InputMethodManager
+
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: InputMethodRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+            .thenReturn(listOf())
+
+        underTest =
+            InputMethodRepositoryImpl(
+                backgroundDispatcher = kosmos.testDispatcher,
+                inputMethodManager = inputMethodManager,
+            )
+    }
+
+    @Test
+    fun enabledInputMethods_noImes_emptyFlow() =
+        testScope.runTest {
+            whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+                .thenReturn(listOf())
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+                .thenReturn(listOf())
+
+            assertThat(underTest.enabledInputMethods(USER_ID, fetchSubtypes = true).count())
+                .isEqualTo(0)
+        }
+
+    @Test
+    fun selectedInputMethodSubtypes_returnsSubtypeList() =
+        testScope.runTest {
+            val subtypeId = 123
+            val isAuxiliary = true
+            whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+                .thenReturn(listOf(mock<InputMethodInfo>()))
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+                .thenReturn(listOf())
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+                .thenReturn(
+                    listOf(
+                        InputMethodSubtype.InputMethodSubtypeBuilder()
+                            .setSubtypeId(subtypeId)
+                            .setIsAuxiliary(isAuxiliary)
+                            .build()
+                    )
+                )
+
+            val result = underTest.selectedInputMethodSubtypes()
+            assertThat(result).hasSize(1)
+            assertThat(result.first().subtypeId).isEqualTo(subtypeId)
+            assertThat(result.first().isAuxiliary).isEqualTo(isAuxiliary)
+        }
+
+    @Test
+    fun showImePicker_forwardsDisplayId() =
+        testScope.runTest {
+            val displayId = 7
+
+            underTest.showInputMethodPicker(displayId, /* showAuxiliarySubtypes = */ true)
+
+            verify(inputMethodManager)
+                .showInputMethodPickerFromSystem(
+                    /* showAuxiliarySubtypes = */ eq(true),
+                    /* displayId = */ eq(displayId)
+                )
+        }
+
+    companion object {
+        private const val USER_ID = 100
+        private val USER_HANDLE = UserHandle.of(USER_ID)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
new file mode 100644
index 0000000..d23ff2a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodInteractorTest : SysuiTestCase() {
+
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val fakeInputMethodRepository = kosmos.fakeInputMethodRepository
+
+    private val underTest = InputMethodInteractor(repository = kosmos.inputMethodRepository)
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_noImes_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(USER_ID)
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_noMatches_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 1, nonAuxiliarySubtypes = 0),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_oneMatch_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_twoMatches_returnsTrue() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_oneWithNonAux_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 2),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_twoWithAux_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 5, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_selectedHasOneSubtype_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.selectedInputMethodSubtypes =
+                listOf(InputMethodModel.Subtype(1, isAuxiliary = false))
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_selectedHasTwoSubtypes_returnsTrue() =
+        testScope.runTest {
+            fakeInputMethodRepository.selectedInputMethodSubtypes =
+                listOf(
+                    InputMethodModel.Subtype(subtypeId = 1, isAuxiliary = false),
+                    InputMethodModel.Subtype(subtypeId = 2, isAuxiliary = false),
+                )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+        }
+
+    @Test
+    fun showImePicker_shownOnCorrectId() =
+        testScope.runTest {
+            val displayId = 7
+
+            underTest.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+
+            assertThat(fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isEqualTo(displayId)
+        }
+
+    private fun createInputMethodWithSubtypes(
+        auxiliarySubtypes: Int,
+        nonAuxiliarySubtypes: Int,
+    ): InputMethodModel {
+        return InputMethodModel(
+            imeId = UUID.randomUUID().toString(),
+            subtypes =
+                List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+                    InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+                }
+        )
+    }
+
+    companion object {
+        private const val USER_ID = 100
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
index e20d3af..ee3e241 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
@@ -43,6 +43,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CameraQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var cameraGestureHelper: CameraGestureHelper
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
index 4ae144c..77e0f4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -42,6 +42,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
 
     @Mock private lateinit var context: Context
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 521dea3..ca64cec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -40,6 +40,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var controller: QRCodeScannerController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 6b7d263..558e7e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -50,6 +50,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var authController: AuthController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
index ae6c5b7..a0b8542 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
@@ -40,6 +40,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DevicePostureRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: DevicePostureRepository
     private lateinit var testScope: TestScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index c01c79d..128b465 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -63,6 +63,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardRepositoryImplTest : SysuiTestCase() {
 
     @Mock private lateinit var statusBarStateController: StatusBarStateController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index ee47c58f4..5f0f24d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -46,6 +46,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class TrustRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var trustManager: TrustManager
     @Captor private lateinit var listener: ArgumentCaptor<TrustManager.TrustListener>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 6828041..9368097 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -45,6 +45,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
 
     val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
index e850456..4695ea4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
@@ -13,6 +13,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardRemotePreviewManagerTest : SysuiTestCase() {
 
     private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index d4dd2ac..0b80ff8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -36,6 +36,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenContentViewModelTest : SysuiTestCase() {
 
     private val kosmos: Kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 30ac344..6fc5be1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -52,11 +52,9 @@
     val testScope = kosmos.testScope
 
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+    val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
     val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
-    val underTest by lazy {
-        kosmos.primaryBouncerToGoneTransitionViewModel
-    }
+    val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt
new file mode 100644
index 0000000..b7b3fdb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
+import com.android.systemui.qs.tiles.impl.fontscaling.qsFontScalingTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FontScalingTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val fontScalingTileConfig = kosmos.qsFontScalingTileConfig
+
+    private val mapper by lazy {
+        FontScalingTileMapper(
+            context.orCreateTestableResources
+                .apply { addOverride(R.drawable.ic_qs_font_scaling, TestStubDrawable()) }
+                .resources,
+            context.theme
+        )
+    }
+
+    @Test
+    fun activeStateMatchesEnabledModel() {
+        val inputModel = FontScalingTileModel
+
+        val outputState = mapper.map(fontScalingTileConfig, inputModel)
+
+        val expectedState = createFontScalingTileState()
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createFontScalingTileState(): QSTileState =
+        QSTileState(
+            {
+                Icon.Loaded(
+                    context.getDrawable(
+                        R.drawable.ic_qs_font_scaling,
+                    )!!,
+                    null
+                )
+            },
+            context.getString(R.string.quick_settings_font_scaling_label),
+            QSTileState.ActivationState.ACTIVE,
+            null,
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            context.getString(R.string.quick_settings_font_scaling_label),
+            null,
+            QSTileState.SideViewIcon.Chevron,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt
new file mode 100644
index 0000000..39bc8a6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FontScalingTileDataInteractorTest : SysuiTestCase() {
+    private val underTest: FontScalingTileDataInteractor = FontScalingTileDataInteractor()
+    private val testUser = UserHandle.of(1)
+
+    @Test
+    fun collectsExactlyOneValue() = runTest {
+        val flowValues by
+            collectValues(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
+
+        runCurrent()
+
+        Truth.assertThat(flowValues.size).isEqualTo(1)
+    }
+
+    @Test
+    fun lastValueIsNotEmpty() = runTest {
+        val flowValue by
+            collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
+
+        runCurrent()
+
+        Truth.assertThat(flowValue).isNotNull()
+    }
+
+    @Test
+    fun isAvailable() = runTest {
+        val availability by collectLastValue(underTest.availability(testUser))
+
+        Truth.assertThat(availability).isTrue()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
new file mode 100644
index 0000000..2384915
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
+
+import android.provider.Settings
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
+import com.android.systemui.statusbar.phone.FakeKeyguardStateController
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
+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.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FontScalingUserActionInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+    private val keyguardStateController = FakeKeyguardStateController()
+
+    private lateinit var underTest: FontScalingTileUserActionInteractor
+
+    @Mock private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var dialog: SystemUIDialog
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable>
+
+    @Before
+    fun setup() {
+        activityStarter = mock<ActivityStarter>()
+        dialogLaunchAnimator = mock<DialogLaunchAnimator>()
+        dialog = mock<SystemUIDialog>()
+        fontScalingDialogDelegate =
+            mock<FontScalingDialogDelegate> { whenever(createDialog()).thenReturn(dialog) }
+        argumentCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+
+        underTest =
+            FontScalingTileUserActionInteractor(
+                kosmos.testScope.coroutineContext,
+                qsTileIntentUserActionHandler,
+                { fontScalingDialogDelegate },
+                keyguardStateController,
+                dialogLaunchAnimator,
+                activityStarter
+            )
+    }
+
+    @Test
+    fun clickTile_screenUnlocked_showDialogAnimationFromView() =
+        kosmos.testScope.runTest {
+            keyguardStateController.isShowing = false
+            val testView = View(context)
+
+            underTest.handleInput(click(FontScalingTileModel, view = testView))
+
+            verify(activityStarter)
+                .executeRunnableDismissingKeyguard(
+                    argumentCaptor.capture(),
+                    eq(null),
+                    eq(true),
+                    eq(true),
+                    eq(false)
+                )
+            argumentCaptor.value.run()
+            verify(dialogLaunchAnimator).showFromView(any(), eq(testView), nullable(), anyBoolean())
+        }
+
+    @Test
+    fun clickTile_onLockScreen_neverShowDialogAnimationFromView_butShowsDialog() =
+        kosmos.testScope.runTest {
+            keyguardStateController.isShowing = true
+            val testView = View(context)
+
+            underTest.handleInput(click(FontScalingTileModel, view = testView))
+
+            verify(activityStarter)
+                .executeRunnableDismissingKeyguard(
+                    argumentCaptor.capture(),
+                    eq(null),
+                    eq(true),
+                    eq(true),
+                    eq(false)
+                )
+            argumentCaptor.value.run()
+            verify(dialogLaunchAnimator, never())
+                .showFromView(any(), eq(testView), nullable(), anyBoolean())
+            verify(dialog).show()
+        }
+
+    @Test
+    fun handleLongClick() =
+        kosmos.testScope.runTest {
+            underTest.handleInput(QSTileInputTestKtx.longClick(FontScalingTileModel))
+
+            Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+            val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+            val actualIntentAction = intentInput.intent.action
+            val expectedIntentAction = Settings.ACTION_TEXT_READING_SETTINGS
+            Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 504ded3..d6d2509 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -76,6 +76,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
@@ -262,6 +264,7 @@
                 simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor },
                 authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor },
                 windowController = mock(),
+                deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
             )
         startable.start()
 
@@ -518,6 +521,17 @@
             assertCurrentScene(SceneKey.Lockscreen)
         }
 
+    @Test
+    fun factoryResetProtectionActive_isNotVisible() =
+        testScope.runTest {
+            val isVisible by collectLastValue(sceneContainerViewModel.isVisible)
+            assertThat(isVisible).isTrue()
+
+            kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isActive = true)
+
+            assertThat(isVisible).isFalse()
+        }
+
     /**
      * Asserts that the current scene in the view-model matches what's expected.
      *
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index b267720..2ad872c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -40,6 +40,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class SceneContainerRepositoryTest : SysuiTestCase() {
 
     private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
index 7ae501d..13b5b54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class WindowRootViewVisibilityRepositoryTest : SysuiTestCase() {
     private val iStatusBarService = mock<IStatusBarService>()
     private val executor = FakeExecutor(FakeSystemClock())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 16cb623..1abbc92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
@@ -112,6 +113,7 @@
                 simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor },
                 authenticationInteractor = dagger.Lazy { authenticationInteractor },
                 windowController = windowController,
+                deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
index cb83e7c..bbbee90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class BcSmartspaceConfigProviderTest : SysuiTestCase() {
     @Mock private lateinit var featureFlags: FeatureFlags
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
index 0b5aea7..089eb43e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
@@ -37,6 +37,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenPreconditionTest : SysuiTestCase() {
     @Mock
     private lateinit var deviceProvisionedController: DeviceProvisionedController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
index bf33010..6616786 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
@@ -52,6 +52,7 @@
 @SmallTest
 @TestableLooper.RunWithLooper
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenTargetFilterTest : SysuiTestCase() {
     @Mock private lateinit var secureSettings: SecureSettings
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
index 8a0400d..f7a8858 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
@@ -38,6 +38,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class RemoteInputRepositoryImplTest : SysuiTestCase() {
     @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
index 12469dd..60da53c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class RemoteInputInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 6a2e317..4d7d5d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -87,21 +87,21 @@
         }
 
     @Test
-    fun updateShadeExpansion() =
+    fun shadeExpansion_goneToShade() =
         testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Gone)
+                )
+            sceneInteractor.setTransitionState(transitionState)
             val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
             assertThat(expandFraction).isEqualTo(0f)
 
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
-                )
-            sceneInteractor.setTransitionState(transitionState)
             sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason")
             val transitionProgress = MutableStateFlow(0f)
             transitionState.value =
                 ObservableTransitionState.Transition(
-                    fromScene = SceneKey.Lockscreen,
+                    fromScene = SceneKey.Gone,
                     toScene = SceneKey.Shade,
                     progress = transitionProgress,
                     isInitiatedByUserInput = false,
@@ -118,4 +118,49 @@
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason")
             assertThat(expandFraction).isWithin(0.01f).of(1f)
         }
+
+    @Test
+    fun shadeExpansion_idleOnLockscreen() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+        }
+
+    @Test
+    fun shadeExpansion_shadeToQs() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Shade)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+
+            sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason")
+            val transitionProgress = MutableStateFlow(0f)
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = SceneKey.Shade,
+                    toScene = SceneKey.QuickSettings,
+                    progress = transitionProgress,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            val steps = 10
+            repeat(steps) { repetition ->
+                val progress = (1f / steps) * (repetition + 1)
+                transitionProgress.value = progress
+                runCurrent()
+                assertThat(expandFraction).isEqualTo(1f)
+            }
+
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason")
+            assertThat(expandFraction).isEqualTo(1f)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index c7411cd..ffe6e6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
 
     private val kosmos = Kosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
index c669c6f..1f6ba29 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
@@ -33,6 +33,10 @@
         mOccluded = occluded;
     }
 
+    public void setShowing(boolean isShowing) {
+        mShowing = isShowing;
+    }
+
     @Override
     public boolean isShowing() {
         return mShowing;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index ce00250..18825a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -28,6 +28,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DisabledWifiRepositoryTest : SysuiTestCase() {
 
     private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 7fbbfc7..84c728c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -43,6 +43,7 @@
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class WifiInteractorImplTest : SysuiTestCase() {
 
     private lateinit var underTest: WifiInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
index ebc81be..4c8bbe0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
@@ -45,6 +45,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserSetupRepositoryTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
index 26c0f80..7ec0a61 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserSetupInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
index b4a0a37..96d1c0d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class BooleanFlowOperatorsTest : SysuiTestCase() {
 
     val kosmos = testKosmos()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 5d85fba..c9e2989 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -264,15 +264,18 @@
         }
     }
 
-    @ProvidesInterface(version = BooleanState.VERSION)
-    public static class BooleanState extends State {
+    /**
+     * Distinguished from [BooleanState] for use-case purposes such as allowing null secondary label
+     */
+    @ProvidesInterface(version = AdapterState.VERSION)
+    class AdapterState extends State {
         public static final int VERSION = 1;
         public boolean value;
         public boolean forceExpandIcon;
 
         @Override
         public boolean copyTo(State other) {
-            final BooleanState o = (BooleanState) other;
+            final AdapterState o = (AdapterState) other;
             final boolean changed = super.copyTo(other)
                     || o.value != value
                     || o.forceExpandIcon != forceExpandIcon;
@@ -291,6 +294,18 @@
 
         @Override
         public State copy() {
+            AdapterState state = new AdapterState();
+            copyTo(state);
+            return state;
+        }
+    }
+
+    @ProvidesInterface(version = BooleanState.VERSION)
+    class BooleanState extends AdapterState {
+        public static final int VERSION = 1;
+
+        @Override
+        public State copy() {
             BooleanState state = new BooleanState();
             copyTo(state);
             return state;
diff --git a/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
new file mode 100644
index 0000000..00b461b
--- /dev/null
+++ b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
new file mode 100644
index 0000000..02486bf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (C) 2024 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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/accessibility_window_magnification_drag_handle_background_change"
+    android:insetBottom="@dimen/magnification_inner_border_margin"
+    android:insetLeft="@dimen/magnification_inner_border_margin"
+    android:insetRight="@dimen/magnification_inner_border_margin"
+    android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
new file mode 100644
index 0000000..bfb7c47
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (C) 2024 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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/accessibility_window_magnification_drag_handle_background"
+    android:insetBottom="@dimen/magnification_inner_border_margin"
+    android:insetLeft="@dimen/magnification_inner_border_margin"
+    android:insetRight="@dimen/magnification_inner_border_margin"
+    android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml
index a8a048d..6286f34 100644
--- a/packages/SystemUI/res/layout/window_magnifier_view.xml
+++ b/packages/SystemUI/res/layout/window_magnifier_view.xml
@@ -117,12 +117,11 @@
         android:id="@+id/drag_handle"
         android:layout_width="@dimen/magnification_drag_view_size"
         android:layout_height="@dimen/magnification_drag_view_size"
-        android:layout_margin="@dimen/magnification_inner_border_margin"
         android:layout_gravity="right|bottom"
         android:padding="@dimen/magnifier_drag_handle_padding"
         android:scaleType="center"
         android:src="@drawable/ic_move_magnification"
-        android:background="@drawable/accessibility_window_magnification_drag_handle_background"/>
+        android:background="@drawable/accessibility_window_magnification_drag_handle_background_inset"/>
 
     <ImageView
         android:id="@+id/close_button"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 17719d1..7a83070 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -991,7 +991,7 @@
 
     <!-- Component name for Home Panel Dream -->
     <string name="config_homePanelDreamComponent" translatable="false">
-        @null
+        com.android.systemui/com.android.systemui.dreams.homecontrols.HomeControlsDreamService
     </string>
 
     <!--
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4209c1f..33bdca3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -903,6 +903,10 @@
         Keep it the same as in Launcher-->
     <dimen name="communal_enforced_rounded_corner_max_radius">16dp</dimen>
 
+    <!-- Width and height used to filter widgets displayed in the communal widget picker -->
+    <dimen name="communal_widget_picker_desired_width">464dp</dimen>
+    <dimen name="communal_widget_picker_desired_height">307dp</dimen>
+
     <!-- The width/height of the unlock icon view on keyguard. -->
     <dimen name="keyguard_lock_height">42dp</dimen>
     <dimen name="keyguard_lock_padding">20dp</dimen>
@@ -1249,7 +1253,7 @@
     <dimen name="magnification_drag_corner_margin">8dp</dimen>
     <dimen name="magnification_frame_move_short">5dp</dimen>
     <dimen name="magnification_frame_move_long">25dp</dimen>
-    <dimen name="magnification_drag_view_size">36dp</dimen>
+    <dimen name="magnification_drag_view_size">70dp</dimen>
     <dimen name="magnification_controls_size">90dp</dimen>
     <dimen name="magnification_switch_button_size">56dp</dimen>
     <dimen name="magnification_switch_button_padding">6dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7943588..8971859 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3314,4 +3314,8 @@
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
     <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
+    <!-- Label for home control panel [CHAR LIMIT=30] -->
+    <string name="home_controls_dream_label">Home Controls</string>
+    <!-- Description for home control panel [CHAR LIMIT=50] -->
+    <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
 </resources>
diff --git a/packages/SystemUI/res/xml/home_controls_dream_metadata.xml b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
new file mode 100644
index 0000000..eb7c79e
--- /dev/null
+++ b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+       android:showClockAndComplications="false"
+       android:previewImage="@drawable/homecontrols_sq"
+    />
\ No newline at end of file
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 326c7ef..4e04af6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -53,6 +53,7 @@
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "SystemUISharedLib-Keyguard",
+        "WindowManager-Shell-shared",
         "tracinglib",
         "androidx.dynamicanimation_dynamicanimation",
         "androidx.concurrent_concurrent-futures",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 88b9c02..a78080f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -38,7 +38,7 @@
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.TransitionInfo;
 
-import com.android.wm.shell.util.CounterRotator;
+import com.android.wm.shell.shared.CounterRotator;
 
 public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub {
     private static final String TAG = "RemoteAnimRunnerCompat";
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index d4ac195..e4d9243 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -22,7 +22,7 @@
 import android.window.TransitionInfo;
 import android.window.TransitionInfo.Change;
 
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.function.Predicate;
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 82410fd..91e6b62 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -39,7 +39,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.DisplaySpecific
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags.REGION_SAMPLING
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -47,7 +47,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockMessageBuffers
@@ -92,7 +91,7 @@
     @Main private val mainExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
     private val clockBuffers: ClockMessageBuffers,
-    private val featureFlags: FeatureFlags,
+    private val featureFlags: FeatureFlagsClassic,
     private val zenModeController: ZenModeController,
 ) {
     var loggers = listOf(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index ad30317..28013c6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -196,6 +196,9 @@
             mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
             mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
             mStatusArea = findViewById(R.id.keyguard_status_area);
+        } else {
+            removeView(findViewById(R.id.lockscreen_clock_view));
+            removeView(findViewById(R.id.lockscreen_clock_view_large));
         }
         onConfigChanged();
     }
@@ -263,6 +266,9 @@
     }
 
     void updateClockTargetRegions() {
+        if (migrateClocksToBlueprint()) {
+            return;
+        }
         if (mClock != null) {
             if (mSmallClockFrame.isLaidOut()) {
                 Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index de7c12d..2db3795 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -246,8 +246,11 @@
     protected void onInit() {
         mKeyguardSliceViewController.init();
 
-        mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
-        mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
+        if (!migrateClocksToBlueprint()) {
+            mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
+            mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
+        }
+
 
         if (!mOnlyClock) {
             mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous
@@ -526,13 +529,15 @@
      */
     void updatePosition(int x, float scale, AnimationProperties props, boolean animate) {
         x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x;
+        if (!migrateClocksToBlueprint()) {
+            PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
+                    x, props, animate);
+            PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
+                    scale, props, animate);
+            PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
+                    scale, props, animate);
 
-        PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
-                x, props, animate);
-        PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
-                scale, props, animate);
-        PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
-                scale, props, animate);
+        }
 
         if (mStatusArea != null) {
             PropertyAnimator.setProperty(mStatusArea, KeyguardStatusAreaView.TRANSLATE_X_AOD,
@@ -550,6 +555,10 @@
             return 0;
         }
 
+        if (migrateClocksToBlueprint()) {
+            return 0;
+        }
+
         if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
             // This gets the expected clock bottom if mLargeClockFrame had a top margin, but it's
             // top margin only contributed to height and didn't move the top of the view (as this
@@ -581,6 +590,9 @@
     }
 
     boolean isClockTopAligned() {
+        if (migrateClocksToBlueprint()) {
+            return mKeyguardClockInteractor.getClockSize().getValue() == LARGE;
+        }
         return mLargeClockFrame.getVisibility() != View.VISIBLE;
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index bddf3b0..d2ad096 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -97,6 +97,26 @@
         )
     }
 
+    fun logUpdateLockScreenUserLockedMsg(
+        userId: Int,
+        userUnlocked: Boolean,
+        encryptedOrLockdown: Boolean,
+    ) {
+        buffer.log(
+            KeyguardIndicationController.TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = userId
+                bool1 = userUnlocked
+                bool2 = encryptedOrLockdown
+            },
+            {
+                "updateLockScreenUserLockedMsg userId=$int1 " +
+                    "userUnlocked:$bool1 encryptedOrLockdown:$bool2"
+            }
+        )
+    }
+
     fun logUpdateBatteryIndication(
         powerIndication: String,
         pluggedIn: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
index 4372826..0f5f869 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
@@ -20,12 +20,11 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.pm.UserInfo;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
 
+import com.android.systemui.res.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory;
@@ -33,7 +32,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.QSUserSwitcherEvent;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -63,7 +61,6 @@
     private final SecureSettings mSecureSettings;
     private final ResetSessionDialogFactory mResetSessionDialogFactory;
     private final GuestSessionNotification mGuestSessionNotification;
-    private final HandlerThread mHandlerThread;
 
     @VisibleForTesting
     public final UserTracker.Callback mUserChangedCallback =
@@ -114,16 +111,13 @@
         mSecureSettings = secureSettings;
         mGuestSessionNotification = guestSessionNotification;
         mResetSessionDialogFactory = resetSessionDialogFactory;
-        mHandlerThread = new HandlerThread("GuestResumeSessionReceiver");
-        mHandlerThread.start();
     }
 
     /**
      * Register this receiver with the {@link BroadcastDispatcher}
      */
     public void register() {
-        mUserTracker.addCallback(mUserChangedCallback,
-                  new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     private void cancelDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 3ca95e1..5171a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 
 import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -28,10 +29,12 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IMagnificationConnection;
@@ -40,6 +43,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.model.SysUiState;
@@ -49,6 +53,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
+import java.util.function.Supplier;
 
 import javax.inject.Inject;
 
@@ -101,19 +106,28 @@
         @Override
         protected WindowMagnificationController createInstance(Display display) {
             final Context windowContext = mContext.createWindowContext(display,
-                    TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+                    Flags.createWindowlessWindowMagnifier()
+                            ? TYPE_ACCESSIBILITY_OVERLAY
+                            : TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
+                    /* options */ null);
             windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
+
+            Supplier<SurfaceControlViewHost> scvhSupplier = () ->
+                    Flags.createWindowlessWindowMagnifier() ? new SurfaceControlViewHost(mContext,
+                            mContext.getDisplay(), new Binder(), TAG) : null;
+
             return new WindowMagnificationController(
                     windowContext,
                     mHandler,
                     new WindowMagnificationAnimationController(windowContext),
-                    new SfVsyncFrameCallbackProvider(),
-                    null,
+                    /* mirrorWindowControl= */ null,
                     new SurfaceControl.Transaction(),
                     mWindowMagnifierCallback,
                     mSysUiState,
-                    WindowManagerGlobal::getWindowSession,
-                    mSecureSettings);
+                    mSecureSettings,
+                    scvhSupplier,
+                    new SfVsyncFrameCallbackProvider(),
+                    WindowManagerGlobal::getWindowSession);
         }
     }
 
@@ -140,7 +154,7 @@
         @Override
         protected MagnificationSettingsController createInstance(Display display) {
             final Context windowContext = mContext.createWindowContext(display,
-                    TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+                    TYPE_ACCESSIBILITY_OVERLAY, /* options */ null);
             windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
             return new MagnificationSettingsController(
                     windowContext,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index dde9f48..d65cd5c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -63,23 +63,27 @@
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowMetrics;
+import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import androidx.annotation.UiThread;
 import androidx.core.math.MathUtils;
 
 import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.util.settings.SecureSettings;
@@ -158,6 +162,15 @@
     private int mMagnificationFrameOffsetX = 0;
     private int mMagnificationFrameOffsetY = 0;
 
+    @Nullable private Supplier<SurfaceControlViewHost> mScvhSupplier;
+
+    /**
+     * SurfaceControlViewHost is used to control the position of the window containing
+     * {@link #mMirrorView}. Using SurfaceControlViewHost instead of a regular window enables
+     * changing the window's position and setting {@link #mMirrorSurface}'s geometry atomically.
+     */
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+
     // The root of the mirrored content
     private SurfaceControl mMirrorSurface;
 
@@ -236,21 +249,21 @@
             @UiContext Context context,
             @NonNull Handler handler,
             @NonNull WindowMagnificationAnimationController animationController,
-            SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
             MirrorWindowControl mirrorWindowControl,
             SurfaceControl.Transaction transaction,
             @NonNull WindowMagnifierCallback callback,
             SysUiState sysUiState,
-            @NonNull Supplier<IWindowSession> globalWindowSessionSupplier,
-            SecureSettings secureSettings) {
+            SecureSettings secureSettings,
+            Supplier<SurfaceControlViewHost> scvhSupplier,
+            SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
+            Supplier<IWindowSession> globalWindowSessionSupplier) {
         mContext = context;
         mHandler = handler;
         mAnimationController = animationController;
-        mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
         mAnimationController.setWindowMagnificationController(this);
-        mSfVsyncFrameProvider = sfVsyncFrameProvider;
         mWindowMagnifierCallback = callback;
         mSysUiState = sysUiState;
+        mScvhSupplier = scvhSupplier;
         mConfiguration = new Configuration(context.getResources().getConfiguration());
         mWindowMagnificationSizePrefs = new WindowMagnificationSizePrefs(mContext);
 
@@ -288,22 +301,78 @@
         mTransaction = transaction;
         mGestureDetector =
                 new MagnificationGestureDetector(mContext, handler, this);
+        mWindowInsetChangeRunnable = this::onWindowInsetChanged;
+        mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
+        mSfVsyncFrameProvider = sfVsyncFrameProvider;
 
         // Initialize listeners.
-        mMirrorViewRunnable = () -> {
-            if (mMirrorView != null) {
-                final Rect oldViewBounds = new Rect(mMirrorViewBounds);
-                mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
-                if (oldViewBounds.width() != mMirrorViewBounds.width()
-                        || oldViewBounds.height() != mMirrorViewBounds.height()) {
-                    mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
-                            new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+        if (Flags.createWindowlessWindowMagnifier()) {
+            mMirrorViewRunnable = new Runnable() {
+                final Rect mPreviousBounds = new Rect();
+
+                @Override
+                public void run() {
+                    if (mMirrorView != null) {
+                        if (mPreviousBounds.width() != mMirrorViewBounds.width()
+                                || mPreviousBounds.height() != mMirrorViewBounds.height()) {
+                            mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+                                    new Rect(0, 0, mMirrorViewBounds.width(),
+                                            mMirrorViewBounds.height())));
+                            mPreviousBounds.set(mMirrorViewBounds);
+                        }
+                        updateSystemUIStateIfNeeded();
+                        mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+                                mDisplayId, mMirrorViewBounds);
+                    }
                 }
-                updateSystemUIStateIfNeeded();
-                mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
-                        mDisplayId, mMirrorViewBounds);
-            }
-        };
+            };
+
+            mMirrorSurfaceViewLayoutChangeListener =
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            mMirrorView.post(this::applyTapExcludeRegion);
+
+            mMirrorViewGeometryVsyncCallback = null;
+        } else {
+            mMirrorViewRunnable = () -> {
+                if (mMirrorView != null) {
+                    final Rect oldViewBounds = new Rect(mMirrorViewBounds);
+                    mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
+                    if (oldViewBounds.width() != mMirrorViewBounds.width()
+                            || oldViewBounds.height() != mMirrorViewBounds.height()) {
+                        mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+                                new Rect(0, 0,
+                                        mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+                    }
+                    updateSystemUIStateIfNeeded();
+                    mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+                            mDisplayId, mMirrorViewBounds);
+                }
+            };
+
+            mMirrorSurfaceViewLayoutChangeListener =
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            mMirrorView.post(this::applyTapExcludeRegion);
+
+            mMirrorViewGeometryVsyncCallback =
+                    l -> {
+                        if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
+                                mMagnificationFrame, mScale)) {
+                            // The final destination for the magnification surface should be at 0,0
+                            // since the ViewRootImpl's position will change
+                            mTmpRect.set(0, 0, mMagnificationFrame.width(),
+                                    mMagnificationFrame.height());
+                            mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
+                                    Surface.ROTATION_0).apply();
+
+                            // Notify source bounds change when the magnifier is not animating.
+                            if (!mAnimationController.isAnimating()) {
+                                mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
+                                        mSourceBounds);
+                            }
+                        }
+                    };
+        }
+
         mMirrorViewLayoutChangeListener =
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     if (!mHandler.hasCallbacks(mMirrorViewRunnable)) {
@@ -311,34 +380,11 @@
                     }
                 };
 
-        mMirrorSurfaceViewLayoutChangeListener =
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
-                        mMirrorView.post(this::applyTapExcludeRegion);
-
-        mMirrorViewGeometryVsyncCallback =
-                l -> {
-                    if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
-                            mMagnificationFrame, mScale)) {
-                        // The final destination for the magnification surface should be at 0,0
-                        // since the ViewRootImpl's position will change
-                        mTmpRect.set(0, 0, mMagnificationFrame.width(),
-                                mMagnificationFrame.height());
-                        mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
-                                Surface.ROTATION_0).apply();
-
-                        // Notify source bounds change when the magnifier is not animating.
-                        if (!mAnimationController.isAnimating()) {
-                            mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
-                                    mSourceBounds);
-                        }
-                    }
-                };
         mUpdateStateDescriptionRunnable = () -> {
             if (isActivated()) {
                 mMirrorView.setStateDescription(formatStateDescription(mScale));
             }
         };
-        mWindowInsetChangeRunnable = this::onWindowInsetChanged;
     }
 
     private void setupMagnificationSizeScaleOptions() {
@@ -448,13 +494,21 @@
         if (mMirrorView != null) {
             mHandler.removeCallbacks(mMirrorViewRunnable);
             mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
-            mWm.removeView(mMirrorView);
+            if (!Flags.createWindowlessWindowMagnifier()) {
+                mWm.removeView(mMirrorView);
+            }
             mMirrorView = null;
         }
 
         if (mMirrorWindowControl != null) {
             mMirrorWindowControl.destroyControl();
         }
+
+        if (mSurfaceControlViewHost != null) {
+            mSurfaceControlViewHost.release();
+            mSurfaceControlViewHost = null;
+        }
+
         mMirrorViewBounds.setEmpty();
         mSourceBounds.setEmpty();
         updateSystemUIStateIfNeeded();
@@ -551,7 +605,11 @@
         if (!isActivated()) return;
         LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
         params.accessibilityTitle = getAccessibilityWindowTitle();
-        mWm.updateViewLayout(mMirrorView, params);
+        if (Flags.createWindowlessWindowMagnifier()) {
+            mSurfaceControlViewHost.relayout(params);
+        } else {
+            mWm.updateViewLayout(mMirrorView, params);
+        }
     }
 
     /**
@@ -602,6 +660,11 @@
     }
 
     private void createMirrorWindow() {
+        if (Flags.createWindowlessWindowMagnifier()) {
+            createWindowlessMirrorWindow();
+            return;
+        }
+
         // The window should be the size the mirrored surface will be but also add room for the
         // border and the drag handle.
         int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
@@ -652,6 +715,68 @@
         addDragTouchListeners();
     }
 
+    private void createWindowlessMirrorWindow() {
+        // The window should be the size the mirrored surface will be but also add room for the
+        // border and the drag handle.
+        int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+        int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+        // TODO delete TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, it shouldn't be needed anymore
+
+        LayoutParams params = new LayoutParams(
+                windowWidth, windowHeight,
+                LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
+                LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | LayoutParams.FLAG_NOT_FOCUSABLE,
+                PixelFormat.TRANSPARENT);
+        params.receiveInsetsIgnoringZOrder = true;
+        params.setTitle(mContext.getString(R.string.magnification_window_title));
+        params.accessibilityTitle = getAccessibilityWindowTitle();
+        params.setTrustedOverlay();
+
+        mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
+        mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
+
+        mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border);
+
+        // Allow taps to go through to the mirror SurfaceView below.
+        mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
+
+        mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
+        mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate());
+        mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> {
+            if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) {
+                mHandler.post(mWindowInsetChangeRunnable);
+            }
+            return v.onApplyWindowInsets(insets);
+        });
+
+        mSurfaceControlViewHost = mScvhSupplier.get();
+        mSurfaceControlViewHost.setView(mMirrorView, params);
+        SurfaceControl surfaceControl = mSurfaceControlViewHost
+                .getSurfacePackage().getSurfaceControl();
+
+        int x = mMagnificationFrame.left - mMirrorSurfaceMargin;
+        int y = mMagnificationFrame.top - mMirrorSurfaceMargin;
+        mTransaction
+                .setCrop(surfaceControl, new Rect(0, 0, windowWidth, windowHeight))
+                .setPosition(surfaceControl, x, y)
+                .setLayer(surfaceControl, Integer.MAX_VALUE)
+                .show(surfaceControl)
+                .apply();
+
+        mMirrorViewBounds.set(x, y, x + windowWidth, y + windowHeight);
+
+        AccessibilityManager accessibilityManager = mContext
+                .getSystemService(AccessibilityManager.class);
+        accessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl);
+
+        SurfaceHolder holder = mMirrorSurfaceView.getHolder();
+        holder.addCallback(this);
+        holder.setFormat(PixelFormat.RGBA_8888);
+        addDragTouchListeners();
+    }
+
     private void onWindowInsetChanged() {
         if (updateSystemGestureInsetsTop()) {
             updateSystemUIStateIfNeeded();
@@ -659,6 +784,11 @@
     }
 
     private void applyTapExcludeRegion() {
+        if (Flags.createWindowlessWindowMagnifier()) {
+            applyTouchableRegion();
+            return;
+        }
+
         // Sometimes this can get posted and run after deleteWindowMagnification() is called.
         if (mMirrorView == null) return;
 
@@ -709,6 +839,51 @@
         return regionInsideDragBorder;
     }
 
+    private void applyTouchableRegion() {
+        // Sometimes this can get posted and run after deleteWindowMagnification() is called.
+        if (mMirrorView == null) return;
+
+        var surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl();
+        surfaceControl.setTouchableRegion(calculateTouchableRegion());
+    }
+
+    private Region calculateTouchableRegion() {
+        Region touchableRegion = new Region(0, 0, mMirrorView.getWidth(), mMirrorView.getHeight());
+
+        Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
+                mMirrorView.getWidth() - mBorderDragSize,
+                mMirrorView.getHeight() - mBorderDragSize);
+        touchableRegion.op(regionInsideDragBorder, Region.Op.DIFFERENCE);
+
+        Rect dragArea = new Rect();
+        mDragView.getHitRect(dragArea);
+
+        Rect topLeftArea = new Rect();
+        mTopLeftCornerView.getHitRect(topLeftArea);
+
+        Rect topRightArea = new Rect();
+        mTopRightCornerView.getHitRect(topRightArea);
+
+        Rect bottomLeftArea = new Rect();
+        mBottomLeftCornerView.getHitRect(bottomLeftArea);
+
+        Rect bottomRightArea = new Rect();
+        mBottomRightCornerView.getHitRect(bottomRightArea);
+
+        Rect closeArea = new Rect();
+        mCloseView.getHitRect(closeArea);
+
+        // Add touchable regions for drag and close
+        touchableRegion.op(dragArea, Region.Op.UNION);
+        touchableRegion.op(topLeftArea, Region.Op.UNION);
+        touchableRegion.op(topRightArea, Region.Op.UNION);
+        touchableRegion.op(bottomLeftArea, Region.Op.UNION);
+        touchableRegion.op(bottomRightArea, Region.Op.UNION);
+        touchableRegion.op(closeArea, Region.Op.UNION);
+
+        return touchableRegion;
+    }
+
     private String getAccessibilityWindowTitle() {
         return mResources.getString(com.android.internal.R.string.android_system_label);
     }
@@ -852,8 +1027,84 @@
      * {@link #mMagnificationFrame}.
      */
     private void modifyWindowMagnification(boolean computeWindowSize) {
-        mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
-        updateMirrorViewLayout(computeWindowSize);
+        if (Flags.createWindowlessWindowMagnifier()) {
+            updateMirrorSurfaceGeometry();
+            updateWindowlessMirrorViewLayout(computeWindowSize);
+        } else {
+            mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
+            updateMirrorViewLayout(computeWindowSize);
+        }
+    }
+
+    /**
+     * Updates {@link #mMirrorSurface}'s geometry. This modifies {@link #mTransaction} but does not
+     * apply it.
+     */
+    @UiThread
+    private void updateMirrorSurfaceGeometry() {
+        if (isActivated() && mMirrorSurface != null
+                && calculateSourceBounds(mMagnificationFrame, mScale)) {
+            // The final destination for the magnification surface should be at 0,0
+            // since the ViewRootImpl's position will change
+            mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height());
+            mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0);
+
+            // Notify source bounds change when the magnifier is not animating.
+            if (!mAnimationController.isAnimating()) {
+                mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds);
+            }
+        }
+    }
+
+    /**
+     * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based
+     * on the position and size of {@link #mMagnificationFrame}.
+     *
+     * @param computeWindowSize set to {@code true} to compute window size with
+     * {@link #mMagnificationFrame}.
+     */
+    @UiThread
+    private void updateWindowlessMirrorViewLayout(boolean computeWindowSize) {
+        if (!isActivated()) {
+            return;
+        }
+
+        final int width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+        final int height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+        final int minX = -mOuterBorderSize;
+        final int maxX = mWindowBounds.right - width + mOuterBorderSize;
+        final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX);
+
+        final int minY = -mOuterBorderSize;
+        final int maxY = mWindowBounds.bottom - height + mOuterBorderSize;
+        final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY);
+
+        if (computeWindowSize) {
+            LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
+            params.width = width;
+            params.height = height;
+            mSurfaceControlViewHost.relayout(params);
+            mTransaction.setCrop(mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(),
+                    new Rect(0, 0, width, height));
+        }
+
+        mMirrorViewBounds.set(x, y, x + width, y + height);
+        mTransaction.setPosition(
+                mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), x, y);
+        if (computeWindowSize) {
+            mSurfaceControlViewHost.getRootSurfaceControl().applyTransactionOnDraw(mTransaction);
+        } else {
+            mTransaction.apply();
+        }
+
+        // If they are not dragging the handle, we can move the drag handle immediately without
+        // disruption. But if they are dragging it, we avoid moving until the end of the drag.
+        if (!mIsDragging) {
+            mMirrorView.post(this::maybeRepositionButton);
+        }
+
+        mMirrorViewRunnable.run();
     }
 
     /**
@@ -1094,7 +1345,7 @@
 
     /**
      * Enables window magnification with specified parameters. If the given scale is <strong>less
-     * than or equal to 1.0f<strong>, then
+     * than or equal to 1.0f</strong>, then
      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
      * be consistent with the behavior of display magnification.
      *
@@ -1110,7 +1361,7 @@
 
     /**
      * Enables window magnification with specified parameters. If the given scale is <strong>less
-     * than 1.0f<strong>, then
+     * than 1.0f</strong>, then
      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
      * be consistent with the behavior of display magnification.
      *
@@ -1426,10 +1677,8 @@
         final FrameLayout.LayoutParams layoutParams =
                 (FrameLayout.LayoutParams) mDragView.getLayoutParams();
 
-        mMirrorView.getBoundsOnScreen(mTmpRect);
-
         final int newGravity;
-        if (mTmpRect.right >= screenEdgeX) {
+        if (mMirrorViewBounds.right >= screenEdgeX) {
             newGravity = Gravity.BOTTOM | Gravity.LEFT;
         } else {
             newGravity = Gravity.BOTTOM | Gravity.RIGHT;
@@ -1449,8 +1698,8 @@
         mSettingsPanelVisibility = settingsPanelIsShown;
 
         mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
-                ? R.drawable.accessibility_window_magnification_drag_handle_background_change
-                : R.drawable.accessibility_window_magnification_drag_handle_background));
+                ? R.drawable.accessibility_window_magnification_drag_handle_background_change_inset
+                : R.drawable.accessibility_window_magnification_drag_handle_background_inset));
 
         PorterDuffColorFilter filter = new PorterDuffColorFilter(
                 mContext.getColor(settingsPanelIsShown
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 135ab35..4047623 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -31,6 +31,10 @@
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionTileDataInteractor
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionUserActionInteractor
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.model.ColorCorrectionTileModel
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.FontScalingTileMapper
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileDataInteractor
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
 import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper
 import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
 import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
@@ -93,6 +97,7 @@
     companion object {
         const val COLOR_CORRECTION_TILE_SPEC = "color_correction"
         const val COLOR_INVERSION_TILE_SPEC = "inversion"
+        const val FONT_SCALING_TILE_SPEC = "font_scaling"
 
         @Provides
         @IntoMap
@@ -155,5 +160,36 @@
                 stateInteractor,
                 mapper,
             )
+
+        @Provides
+        @IntoMap
+        @StringKey(FONT_SCALING_TILE_SPEC)
+        fun provideFontScalingTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(FONT_SCALING_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.ic_qs_font_scaling,
+                        labelRes = R.string.quick_settings_font_scaling_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject FontScaling Tile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(FONT_SCALING_TILE_SPEC)
+        fun provideFontScalingTileViewModel(
+            factory: QSTileViewModelFactory.Static<FontScalingTileModel>,
+            mapper: FontScalingTileMapper,
+            stateInteractor: FontScalingTileDataInteractor,
+            userActionInteractor: FontScalingTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(FONT_SCALING_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 49f34f1..454ed27 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -39,7 +39,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.onSubscriberAdded
 import com.android.systemui.util.time.SystemClock
 import dagger.Binds
 import dagger.Module
@@ -54,7 +54,6 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
@@ -355,10 +354,7 @@
                     userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
                     // Emits a value only when the number of downstream subscribers of this flow
                     // increases.
-                    flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
-                        ->
-                        current > previous
-                    },
+                    flow.onSubscriberAdded(),
                 ) { selectedUserId, _ ->
                     selectedUserId
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
index 70be0f2..a742c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
@@ -19,22 +19,15 @@
 import android.hardware.biometrics.AuthenticateOptions
 import android.hardware.biometrics.IBiometricContextListener
 import android.util.Log
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.display.data.repository.DeviceStateRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
-import com.android.systemui.unfold.updates.FoldStateProvider
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.catch
@@ -80,15 +73,11 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    private val foldProvider: FoldStateProvider,
+    deviceStateRepository: DeviceStateRepository,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) : LogContextInteractor {
 
-    init {
-        applicationScope.launch { foldProvider.start() }
-    }
-
     override val displayState =
         keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
             when (it.to) {
@@ -123,32 +112,21 @@
             .distinctUntilChanged()
 
     override val foldState: Flow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : FoldStateProvider.FoldUpdatesListener {
-                        override fun onHingeAngleUpdate(angle: Float) {}
-
-                        override fun onFoldUpdate(@FoldStateProvider.FoldUpdate update: Int) {
-                            val loggedState =
-                                when (update) {
-                                    FOLD_UPDATE_FINISH_HALF_OPEN ->
-                                        IBiometricContextListener.FoldState.HALF_OPENED
-                                    FOLD_UPDATE_FINISH_FULL_OPEN ->
-                                        IBiometricContextListener.FoldState.FULLY_OPENED
-                                    FOLD_UPDATE_FINISH_CLOSED ->
-                                        IBiometricContextListener.FoldState.FULLY_CLOSED
-                                    else -> null
-                                }
-                            if (loggedState != null) {
-                                trySendWithFailureLogging(loggedState, TAG)
-                            }
-                        }
-                    }
-
-                foldProvider.addCallback(callback)
-                trySendWithFailureLogging(IBiometricContextListener.FoldState.UNKNOWN, TAG)
-                awaitClose { foldProvider.removeCallback(callback) }
+        deviceStateRepository.state
+            .map {
+                when (it) {
+                    DeviceStateRepository.DeviceState.UNFOLDED,
+                    DeviceStateRepository.DeviceState.REAR_DISPLAY,
+                    DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY ->
+                        IBiometricContextListener.FoldState.FULLY_OPENED
+                    DeviceStateRepository.DeviceState.FOLDED ->
+                        IBiometricContextListener.FoldState.FULLY_CLOSED
+                    DeviceStateRepository.DeviceState.HALF_FOLDED ->
+                        IBiometricContextListener.FoldState.HALF_OPENED
+                    else -> IBiometricContextListener.FoldState.UNKNOWN
+                }
             }
+            .distinctUntilChanged()
             .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
 
     override fun addBiometricContextListener(listener: IBiometricContextListener): Job {
@@ -178,6 +156,3 @@
         private const val TAG = "ContextRepositoryImpl"
     }
 }
-
-private val WakefulnessLifecycle.isAwake: Boolean
-    get() = wakefulness == WakefulnessLifecycle.WAKEFULNESS_AWAKE
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index 7265c0c..d849b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -21,8 +21,6 @@
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 
 /** Provides access to bouncer-related application state. */
 @SysUISingleton
@@ -31,15 +29,10 @@
 constructor(
     private val flags: FeatureFlagsClassic,
 ) {
-    private val _message = MutableStateFlow<String?>(null)
     /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<String?> = _message.asStateFlow()
+    val message = MutableStateFlow<String?>(null)
 
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
     val isUserSwitcherVisible: Boolean
         get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
-
-    fun setMessage(message: String?) {
-        _message.value = message
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index c8ce245..d8be1af 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -120,7 +120,7 @@
     }
 
     fun setMessage(message: String?) {
-        repository.setMessage(message)
+        repository.message.value = message
     }
 
     /**
@@ -129,13 +129,13 @@
      */
     fun resetMessage() {
         applicationScope.launch {
-            repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
+            setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
         }
     }
 
     /** Removes the user-facing message. */
     fun clearMessage() {
-        repository.setMessage(null)
+        setMessage(null)
     }
 
     /**
@@ -196,7 +196,7 @@
      * message without having the attempt trigger lockout.
      */
     private suspend fun showWrongInputMessage() {
-        repository.setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
+        setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
     }
 
     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 4d686a1..4466cbb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -34,7 +34,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -66,8 +68,10 @@
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val bouncerInteractor: BouncerInteractor,
+    private val inputMethodInteractor: InputMethodInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
     flags: SceneContainerFlags,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
@@ -346,8 +350,10 @@
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
                     viewModelScope = newViewModelScope,
-                    interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    interactor = bouncerInteractor,
+                    inputMethodInteractor = inputMethodInteractor,
+                    selectedUserInteractor = selectedUserInteractor,
                 )
             is AuthenticationMethodModel.Pattern ->
                 PatternBouncerViewModel(
@@ -467,11 +473,13 @@
         @Application applicationScope: CoroutineScope,
         @Main mainDispatcher: CoroutineDispatcher,
         bouncerInteractor: BouncerInteractor,
+        imeInteractor: InputMethodInteractor,
         simBouncerInteractor: SimBouncerInteractor,
+        actionButtonInteractor: BouncerActionButtonInteractor,
         authenticationInteractor: AuthenticationInteractor,
+        selectedUserInteractor: SelectedUserInteractor,
         flags: SceneContainerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
-        actionButtonInteractor: BouncerActionButtonInteractor,
         clock: SystemClock,
         devicePolicyManager: DevicePolicyManager,
     ): BouncerViewModel {
@@ -480,8 +488,10 @@
             applicationScope = applicationScope,
             mainDispatcher = mainDispatcher,
             bouncerInteractor = bouncerInteractor,
+            inputMethodInteractor = imeInteractor,
             simBouncerInteractor = simBouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            selectedUserInteractor = selectedUserInteractor,
             flags = flags,
             selectedUser = userSwitcherViewModel.selectedUser,
             users = userSwitcherViewModel.users,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 5c9c997..1c8b84d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,23 +16,32 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
 import com.android.systemui.res.R
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.onSubscriberAdded
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the password bouncer UI. */
 class PasswordBouncerViewModel(
     viewModelScope: CoroutineScope,
-    interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    interactor: BouncerInteractor,
+    private val inputMethodInteractor: InputMethodInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
@@ -49,6 +58,9 @@
 
     override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
 
+    /** Informs the UI whether the input method switcher button should be visible. */
+    val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow()
+
     /** Whether the text field element currently has focus. */
     private val isTextFieldFocused = MutableStateFlow(false)
 
@@ -87,6 +99,13 @@
         _password.value = newPassword
     }
 
+    /** Notifies that the user clicked the button to change the input method. */
+    fun onImeSwitcherButtonClicked(displayId: Int) {
+        viewModelScope.launch {
+            inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+        }
+    }
+
     /** Notifies that the user has pressed the key for attempting to authenticate the password. */
     fun onAuthenticateKeyPressed() {
         if (_password.value.isNotEmpty()) {
@@ -103,4 +122,35 @@
     fun onTextFieldFocusChanged(isFocused: Boolean) {
         isTextFieldFocused.value = isFocused
     }
+
+    /**
+     * Whether the input method switcher button should be displayed in the password bouncer UI. The
+     * value may be stale at the moment of subscription to this flow, but it is guaranteed to be
+     * shortly updated with a fresh value.
+     *
+     * Note: Each added subscription triggers an IPC call in the background, so this should only be
+     * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown).
+     */
+    private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> {
+        val isImeSwitcherButtonVisible = MutableStateFlow(value = false)
+        viewModelScope.launch {
+            // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever
+            // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
+            combine(
+                    // InputMethodManagerService sometimes takes some time to update its internal
+                    // state when the selected user changes. As a workaround, delay fetching the IME
+                    // info.
+                    selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
+                    isImeSwitcherButtonVisible.onSubscriberAdded()
+                ) { selectedUserId, _ ->
+                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
+                }
+                .collect { isImeSwitcherButtonVisible.value = it }
+        }
+        return isImeSwitcherButtonVisible.asStateFlow()
+    }
+
+    companion object {
+        @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
new file mode 100644
index 0000000..f7ba5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
+ * conditions are met.
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@SysUISingleton
+class CommunalSceneStartable
+@Inject
+constructor(
+    private val dockManager: DockManager,
+    private val communalInteractor: CommunalInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+    override fun start() {
+        // Handle automatically switching based on keyguard state.
+        keyguardTransitionInteractor.startedKeyguardTransitionStep
+            .mapLatest(::determineSceneAfterTransition)
+            .filterNotNull()
+            // TODO(b/322787129): Also set a custom transition animation here to avoid the regular
+            // slide-in animation when setting the scene programmatically
+            .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) }
+            .launchIn(applicationScope)
+
+        // Handle automatically switching to communal when docked.
+        dockManager
+            .retrieveIsDocked()
+            // Allow some time after docking to ensure the dream doesn't start. If the dream
+            // starts, then we don't want to automatically transition to glanceable hub.
+            .debounce(DOCK_DEBOUNCE_DELAY)
+            .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair)
+            .onEach { (docked, lastStartedState) ->
+                if (docked && lastStartedState == KeyguardState.LOCKSCREEN) {
+                    communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                }
+            }
+            .launchIn(bgScope)
+    }
+
+    private suspend fun determineSceneAfterTransition(
+        lastStartedTransition: TransitionStep,
+    ): CommunalSceneKey? {
+        val to = lastStartedTransition.to
+        val from = lastStartedTransition.from
+        val docked = dockManager.isDocked
+
+        return when {
+            to == KeyguardState.DREAMING -> CommunalSceneKey.Blank
+            docked && to == KeyguardState.LOCKSCREEN && from != KeyguardState.GLANCEABLE_HUB -> {
+                CommunalSceneKey.Communal
+            }
+            to == KeyguardState.GONE -> CommunalSceneKey.Blank
+            !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
+                // If the user taps the screen and wakes the device within this timeout, we don't
+                // want to dismiss the hub
+                delay(AWAKE_DEBOUNCE_DELAY)
+                CommunalSceneKey.Blank
+            }
+            else -> null
+        }
+    }
+
+    companion object {
+        val AWAKE_DEBOUNCE_DELAY = 5.seconds
+        val DOCK_DEBOUNCE_DELAY = 1.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
index ab0a2d0d..d7f163b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.dagger.CommunalLog
@@ -35,6 +36,7 @@
 import dagger.Provides
 import java.util.Optional
 import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
 
 @Module
 interface CommunalWidgetRepositoryModule {
@@ -52,10 +54,19 @@
         @Provides
         fun provideCommunalAppWidgetHost(
             @Application context: Context,
+            @Background backgroundScope: CoroutineScope,
             interactionHandler: WidgetInteractionHandler,
             @Main looper: Looper,
+            @CommunalLog logBuffer: LogBuffer,
         ): CommunalAppWidgetHost {
-            return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper)
+            return CommunalAppWidgetHost(
+                context,
+                backgroundScope,
+                APP_WIDGET_HOST_ID,
+                interactionHandler,
+                looper,
+                logBuffer,
+            )
         }
 
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 80fee64..192c9001 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -129,9 +129,18 @@
             .distinctUntilChanged()
 
     /**
-     * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the
-     * [CommunalSceneKey.Communal].
+     * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is
+     * the [CommunalSceneKey.Communal].
+     *
+     * This will be true as soon as the desired scene is set programmatically or at whatever point
+     * during a fling that SceneTransitionLayout determines that the end state will be the communal
+     * scene. The value also does not change while flinging away until the target scene is no longer
+     * communal.
+     *
+     * If you need a flow that is only true when communal is fully showing and not in transition,
+     * use [isIdleOnCommunal].
      */
+    // TODO(b/323215860): rename to something more appropriate after cleaning up usages
     val isCommunalShowing: Flow<Boolean> =
         communalRepository.desiredScene.map { it == CommunalSceneKey.Communal }
 
@@ -146,6 +155,16 @@
             it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal
         }
 
+    /**
+     * Flow that emits a boolean if any portion of the communal UI is visible at all.
+     *
+     * This flow will be true during any transition and when idle on the communal scene.
+     */
+    val isCommunalVisible: Flow<Boolean> =
+        communalRepository.transitionState.map {
+            !(it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Blank)
+        }
+
     /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */
     fun onSceneChanged(newScene: CommunalSceneKey) {
         communalRepository.setDesiredScene(newScene)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 61db026..5f1d89e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -22,14 +22,31 @@
 import android.content.Context
 import android.os.Looper
 import android.widget.RemoteViews
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
 
 /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */
 class CommunalAppWidgetHost(
     context: Context,
+    private val backgroundScope: CoroutineScope,
     hostId: Int,
     interactionHandler: RemoteViews.InteractionHandler,
-    looper: Looper
+    looper: Looper,
+    logBuffer: LogBuffer,
 ) : AppWidgetHost(context, hostId, interactionHandler, looper) {
+
+    private val logger = Logger(logBuffer, TAG)
+
+    private val _appWidgetIdToRemove = MutableSharedFlow<Int>()
+
+    /** App widget ids that have been removed and no longer available. */
+    val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
+
     override fun onCreateView(
         context: Context,
         appWidgetId: Int,
@@ -52,4 +69,15 @@
         // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView`
         return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView
     }
+
+    override fun onAppWidgetRemoved(appWidgetId: Int) {
+        backgroundScope.launch {
+            logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId }
+            _appWidgetIdToRemove.emit(appWidgetId)
+        }
+    }
+
+    companion object {
+        private const val TAG = "CommunalAppWidgetHost"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 586df32..fb9abeb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -40,6 +40,7 @@
     @Background private val bgScope: CoroutineScope,
     @Main private val uiDispatcher: CoroutineDispatcher
 ) : CoreStartable {
+
     override fun start() {
         or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
             // Only trigger updates on state changes, ignoring the initial false value.
@@ -47,6 +48,10 @@
             .filter { (previous, new) -> previous != new }
             .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) }
             .launchIn(bgScope)
+
+        appWidgetHost.appWidgetIdToRemove
+            .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) }
+            .launchIn(bgScope)
     }
 
     private suspend fun updateAppWidgetHostActive(active: Boolean) =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index ad1327e..eb6dc43 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -29,8 +29,10 @@
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 /** An Activity for editing the widgets that appear in hub mode. */
@@ -44,8 +46,10 @@
 ) : ComponentActivity() {
     companion object {
         private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
-        private const val EXTRA_FILTER_STRATEGY = "filter_strategy"
-        private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1
+        private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
+
+        private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
+
         private const val TAG = "EditWidgetsActivity"
         const val EXTRA_PRESELECTED_KEY = "preselected_key"
     }
@@ -110,11 +114,19 @@
                     ?.let { packageName ->
                         try {
                             addWidgetActivityLauncher.launch(
-                                Intent(Intent.ACTION_PICK).also {
-                                    it.setPackage(packageName)
-                                    it.putExtra(
-                                        EXTRA_FILTER_STRATEGY,
-                                        FILTER_STRATEGY_GLANCEABLE_HUB
+                                Intent(Intent.ACTION_PICK).apply {
+                                    setPackage(packageName)
+                                    putExtra(
+                                        EXTRA_DESIRED_WIDGET_WIDTH,
+                                        resources.getDimensionPixelSize(
+                                            R.dimen.communal_widget_picker_desired_width
+                                        )
+                                    )
+                                    putExtra(
+                                        EXTRA_DESIRED_WIDGET_HEIGHT,
+                                        resources.getDimensionPixelSize(
+                                            R.dimen.communal_widget_picker_desired_height
+                                        )
                                     )
                                 }
                             )
@@ -126,6 +138,7 @@
             },
             onEditDone = {
                 try {
+                    communalViewModel.onSceneChanged(CommunalSceneKey.Communal)
                     checkNotNull(windowManagerService).lockNow(/* options */ null)
                     finish()
                 } catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 95233f7..5ee2045 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,12 +24,14 @@
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.CommunalSceneStartable
 import com.android.systemui.communal.log.CommunalLoggerStartable
 import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
 import com.android.systemui.dreams.DreamMonitor
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
@@ -50,7 +52,6 @@
 import com.android.systemui.statusbar.ImmersiveModeConfirmation
 import com.android.systemui.statusbar.gesture.GesturePointerEventListener
 import com.android.systemui.statusbar.notification.InstantAppNotifier
-import com.android.systemui.statusbar.phone.KeyguardLiftController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener
 import com.android.systemui.stylus.StylusUsiPowerStartable
@@ -224,12 +225,6 @@
     @ClassKey(WMShell::class)
     abstract fun bindWMShell(sysui: WMShell): CoreStartable
 
-    /** Inject into KeyguardLiftController.  */
-    @Binds
-    @IntoMap
-    @ClassKey(KeyguardLiftController::class)
-    abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
-
     /** Inject into MediaTttSenderCoordinator. */
     @Binds
     @IntoMap
@@ -328,8 +323,18 @@
 
     @Binds
     @IntoMap
+    @ClassKey(CommunalSceneStartable::class)
+    abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(CommunalAppWidgetHostStartable::class)
     abstract fun bindCommunalAppWidgetHostStartable(
         impl: CommunalAppWidgetHostStartable
     ): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamStartable::class)
+    abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2587e2d..efcbd47 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -57,6 +57,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagDependenciesModule;
 import com.android.systemui.flags.FlagsModule;
+import com.android.systemui.inputmethod.InputMethodModule;
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
@@ -193,6 +194,7 @@
         FlagsModule.class,
         FlagDependenciesModule.class,
         FooterActionsModule.class,
+        InputMethodModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
         KeyguardBlueprintModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
new file mode 100644
index 0000000..1fd7d00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 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.deviceentry.ui.binder
+
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEvent
+import android.hardware.TriggerEventListener
+import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.Assert
+import com.android.systemui.util.sensors.AsyncSensorManager
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Triggers face auth and active unlock on lift when the device is showing the lock screen or
+ * bouncer. Only initialized if face auth is supported on the device. Not to be confused with the
+ * lift to wake gesture which is handled by {@link com.android.server.policy.PhoneWindowManager}.
+ */
+@SysUISingleton
+class LiftToRunFaceAuthBinder
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val packageManager: PackageManager,
+    private val asyncSensorManager: AsyncSensorManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    keyguardInteractor: KeyguardInteractor,
+    primaryBouncerInteractor: PrimaryBouncerInteractor,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    powerInteractor: PowerInteractor,
+) : CoreStartable {
+
+    private var pickupSensor: Sensor? = null
+    private val isListening: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    private val stoppedListening: Flow<Unit> = isListening.filterNot { it }.map {} // map to Unit
+
+    private val onAwakeKeyguard: Flow<Boolean> =
+        combine(
+            powerInteractor.isInteractive,
+            keyguardInteractor.isKeyguardVisible,
+        ) { isInteractive, isKeyguardVisible ->
+            isInteractive && isKeyguardVisible
+        }
+    private val bouncerShowing: Flow<Boolean> =
+        combine(
+            primaryBouncerInteractor.isShowing,
+            alternateBouncerInteractor.isVisible,
+        ) { primaryBouncerShowing, alternateBouncerShowing ->
+            primaryBouncerShowing || alternateBouncerShowing
+        }
+    private val listenForPickupSensor: Flow<Boolean> =
+        combine(
+            stoppedListening,
+            bouncerShowing,
+            onAwakeKeyguard,
+        ) { _, bouncerShowing, onAwakeKeyguard ->
+            (onAwakeKeyguard || bouncerShowing) &&
+                deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
+        }
+
+    override fun start() {
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
+            init()
+        }
+    }
+
+    private fun init() {
+        pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
+        scope.launch {
+            listenForPickupSensor.collect { listenForPickupSensor ->
+                updateListeningState(listenForPickupSensor)
+            }
+        }
+    }
+
+    private val listener: TriggerEventListener =
+        object : TriggerEventListener() {
+            override fun onTrigger(event: TriggerEvent?) {
+                Assert.isMainThread()
+                deviceEntryFaceAuthInteractor.onDeviceLifted()
+                keyguardUpdateMonitor.requestActiveUnlock(
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
+                    "KeyguardLiftController"
+                )
+
+                // Not listening anymore since trigger events unregister themselves
+                isListening.value = false
+            }
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("LiftToRunFaceAuthBinder:")
+        pw.println("  pickupSensor: $pickupSensor")
+        pw.println("  isListening: ${isListening.value}")
+    }
+
+    private fun updateListeningState(shouldListen: Boolean) {
+        if (pickupSensor == null) {
+            return
+        }
+        if (shouldListen != isListening.value) {
+            isListening.value = shouldListen
+
+            if (shouldListen) {
+                asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
+            } else {
+                asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 0656933..ba74742 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams.dagger;
 
 import android.annotation.Nullable;
+import android.app.Service;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -30,12 +31,18 @@
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
+import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
+import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
 import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
 import com.android.systemui.res.R;
 import com.android.systemui.touch.TouchInsetManager;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
 
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -88,6 +95,15 @@
     }
 
     /**
+     * Provides Home Controls Dream Service
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamService.class)
+    Service bindHomeControlsDreamService(
+            HomeControlsDreamService service);
+
+    /**
      * Provides a touch inset manager for dreams.
      */
     @Provides
@@ -151,4 +167,9 @@
     static String providesDreamOverlayWindowTitle(@Main Resources resources) {
         return resources.getString(R.string.app_label);
     }
+
+    /** Provides activity for dream service */
+    @Binds
+    DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl);
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
new file mode 100644
index 0000000..b35b7f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+
+fun interface DreamActivityProvider {
+    /**
+     * Provides abstraction for getting the activity associated with a dream service, so that the
+     * activity can be mocked in tests.
+     */
+    fun getActivity(dreamService: DreamService): Activity?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
new file mode 100644
index 0000000..0854e93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+import javax.inject.Inject
+
+class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
+    override fun getActivity(dreamService: DreamService): Activity {
+        return dreamService.activity
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
new file mode 100644
index 0000000..e04a505
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols
+
+import android.content.Intent
+import android.service.controls.ControlsProviderService
+import android.service.dreams.DreamService
+import android.window.TaskFragmentInfo
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import javax.inject.Inject
+
+class HomeControlsDreamService
+@Inject
+constructor(
+    private val controlsSettingsRepository: ControlsSettingsRepository,
+    private val taskFragmentFactory: TaskFragmentComponent.Factory,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    private val dreamActivityProvider: DreamActivityProvider,
+    @DreamLog logBuffer: LogBuffer
+) : DreamService() {
+    private lateinit var taskFragmentComponent: TaskFragmentComponent
+
+    private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        val activity = dreamActivityProvider.getActivity(this)
+        if (activity == null) {
+            finish()
+            return
+        }
+        taskFragmentComponent =
+            taskFragmentFactory
+                .create(
+                    activity = activity,
+                    onCreateCallback = this::onTaskFragmentCreated,
+                    onInfoChangedCallback = this::onTaskFragmentInfoChanged,
+                    hide = { finish() }
+                )
+                .apply { createTaskFragment() }
+    }
+
+    private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) {
+        if (taskFragmentInfo.isEmpty) {
+            logger.d("Finishing dream due to TaskFragment being empty")
+            finish()
+        }
+    }
+
+    private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) {
+        val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+        val componentName = homeControlsComponentInteractor.panelComponent.value
+        logger.d("Starting embedding $componentName")
+        val intent =
+            Intent().apply {
+                component = componentName
+                putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
+                putExtra(
+                    ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+                    ControlsProviderService.CONTROLS_SURFACE_DREAM
+                )
+            }
+        taskFragmentComponent.startActivityInTaskFragment(intent)
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        taskFragmentComponent.destroy()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
new file mode 100644
index 0000000..6cd94c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.service.controls.flags.Flags.homePanelDream
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class HomeControlsDreamStartable
+@Inject
+constructor(
+    private val context: Context,
+    private val packageManager: PackageManager,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+
+    override fun start() {
+        if (!homePanelDream()) return
+        bgScope.launch {
+            homeControlsComponentInteractor.panelComponent.collect { selectedPanelComponent ->
+                setEnableHomeControlPanel(selectedPanelComponent != null)
+            }
+        }
+    }
+
+    private fun setEnableHomeControlPanel(enabled: Boolean) {
+        val packageState =
+            if (enabled) {
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+            } else {
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+            }
+        packageManager.setComponentEnabledSetting(
+            componentName,
+            packageState,
+            PackageManager.DONT_KILL_APP
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
new file mode 100644
index 0000000..6f7dcb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Binder
+import android.window.TaskFragmentCreationParams
+import android.window.TaskFragmentInfo
+import android.window.TaskFragmentOperation
+import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
+import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
+import android.window.TaskFragmentOrganizer
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN
+import android.window.TaskFragmentTransaction
+import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED
+import android.window.WindowContainerTransaction
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit
+
+/** Wrapper around TaskFragmentOrganizer for managing a task fragment within an activity */
+class TaskFragmentComponent
+@AssistedInject
+constructor(
+    @Assisted private val activity: Activity,
+    @Assisted("onCreateCallback") private val onCreateCallback: FragmentInfoCallback,
+    @Assisted("onInfoChangedCallback") private val onInfoChangedCallback: FragmentInfoCallback,
+    @Assisted private val hide: () -> Unit,
+    @Main private val executor: DelayableExecutor,
+) {
+
+    @AssistedFactory
+    fun interface Factory {
+        fun create(
+            activity: Activity,
+            @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback,
+            @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback,
+            hide: () -> Unit
+        ): TaskFragmentComponent
+    }
+
+    private val fragmentToken = Binder()
+    private val organizer: TaskFragmentOrganizer =
+        object : TaskFragmentOrganizer(executor) {
+
+                override fun onTransactionReady(transaction: TaskFragmentTransaction) {
+                    handleTransactionReady(transaction)
+                }
+            }
+            .apply { registerOrganizer(true /* isSystemOrganizer */) }
+
+    private fun handleTransactionReady(transaction: TaskFragmentTransaction) {
+        val resultT = WindowContainerTransaction()
+
+        for (change in transaction.changes) {
+            change.taskFragmentInfo?.let { taskFragmentInfo ->
+                if (taskFragmentInfo.fragmentToken == fragmentToken) {
+                    when (change.type) {
+                        TYPE_TASK_FRAGMENT_APPEARED -> {
+                            resultT.addTaskFragmentOperation(
+                                fragmentToken,
+                                TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK)
+                                    .build()
+                            )
+
+                            onCreateCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
+                            onInfoChangedCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_VANISHED -> {
+                            hide()
+                        }
+                        TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
+                        TYPE_TASK_FRAGMENT_ERROR -> {
+                            hide()
+                        }
+                        TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unknown TaskFragmentEvent=" + change.type
+                            )
+                    }
+                }
+            }
+        }
+        organizer.onTransactionHandled(
+            transaction.transactionToken,
+            resultT,
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    /** Creates the task fragment */
+    fun createTaskFragment() {
+        val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds)
+        val fragmentOptions =
+            TaskFragmentCreationParams.Builder(
+                    organizer.organizerToken,
+                    fragmentToken,
+                    activity.activityToken!!
+                )
+                .setInitialRelativeBounds(taskBounds)
+                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+                .build()
+        organizer.applyTransaction(
+            WindowContainerTransaction().createTaskFragment(fragmentOptions),
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    private fun WindowContainerTransaction.startActivity(intent: Intent) =
+        this.startActivityInTaskFragment(fragmentToken, activity.activityToken!!, intent, null)
+
+    /** Starts the provided activity in the fragment and move it to the background */
+    fun startActivityInTaskFragment(intent: Intent) {
+        organizer.applyTransaction(
+            WindowContainerTransaction().startActivity(intent),
+            TASK_FRAGMENT_TRANSIT_OPEN,
+            false
+        )
+    }
+
+    /** Destroys the task fragment */
+    fun destroy() {
+        organizer.applyTransaction(
+            WindowContainerTransaction()
+                .addTaskFragmentOperation(
+                    fragmentToken,
+                    TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build()
+                ),
+            TASK_FRAGMENT_TRANSIT_CLOSE,
+            false
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
new file mode 100644
index 0000000..91e0547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols.domain.interactor
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class HomeControlsComponentInteractor
+@Inject
+constructor(
+    private val selectedComponentRepository: SelectedComponentRepository,
+    private val controlsComponent: ControlsComponent,
+    private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+    userRepository: UserRepository,
+    @Background private val bgScope: CoroutineScope
+) {
+    private val controlsListingController =
+        controlsComponent.getControlsListingController().getOrNull()
+
+    /** Gets the current user's selected panel, or null if there isn't one */
+    private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> =
+        userRepository.selectedUserInfo
+            .flatMapLatest { user ->
+                selectedComponentRepository.selectedComponentFlow(user.userHandle)
+            }
+            .map { if (it?.isPanel == true) it else null }
+
+    /** Gets all the available panels which are authorized by the user */
+    private fun allPanelItem(): Flow<List<PanelComponent>> {
+        if (controlsListingController == null) {
+            return emptyFlow()
+        }
+        return conflatedCallbackFlow {
+                val listener =
+                    object : ControlsListingController.ControlsListingCallback {
+                        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                            trySend(serviceInfos)
+                        }
+                    }
+                controlsListingController.addCallback(listener)
+                awaitClose { controlsListingController.removeCallback(listener) }
+            }
+            .onStart { emit(controlsListingController.getCurrentServices()) }
+            .map { serviceInfos ->
+                val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
+                serviceInfos.mapNotNull {
+                    val panelActivity = it.panelActivity
+                    if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+                        PanelComponent(it.componentName, panelActivity)
+                    } else {
+                        null
+                    }
+                }
+            }
+    }
+    val panelComponent: StateFlow<ComponentName?> =
+        combine(allPanelItem(), selectedItem) { items, selected ->
+                val item =
+                    items.firstOrNull { it.componentName == selected?.componentName }
+                        ?: items.firstOrNull()
+                item?.panelActivity
+            }
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+
+    data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index c9b56a2..05279fc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -23,7 +23,6 @@
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 
-import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import java.util.Optional;
@@ -34,17 +33,14 @@
 /** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/
 public class CommunalTouchHandler implements DreamTouchHandler {
     private final int mInitiationWidth;
-    private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final Optional<CentralSurfaces> mCentralSurfaces;
 
     @Inject
     public CommunalTouchHandler(
             Optional<CentralSurfaces> centralSurfaces,
-            NotificationShadeWindowController notificationShadeWindowController,
             @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) {
         mInitiationWidth = initiationWidth;
         mCentralSurfaces = centralSurfaces;
-        mNotificationShadeWindowController = notificationShadeWindowController;
     }
 
     @Override
@@ -60,9 +56,8 @@
     }
 
     private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) {
-        // Force the notification shade window open (otherwise the hub won't show while swiping).
-        mNotificationShadeWindowController.setForcePluginOpen(true, this);
-
+        // Notification shade window has its own logic to be visible if the hub is open, no need to
+        // do anything here other than send touch events over.
         session.registerInputListener(ev -> {
             surfaces.handleDreamTouch((MotionEvent) ev);
             if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1540423..df0566e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import javax.inject.Inject
@@ -45,6 +47,7 @@
         // Internal notification frontend dependencies
         NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+        NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
 
         // Internal keyguard dependencies
         KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt
new file mode 100644
index 0000000..bac48f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.inputmethod
+
+import com.android.systemui.inputmethod.data.repository.InputMethodRepositoryModule
+import dagger.Module
+
+/** Module for providing objects exposed by the input method package. */
+@Module(
+    includes =
+        [
+            InputMethodRepositoryModule::class,
+        ],
+)
+object InputMethodModule
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
new file mode 100644
index 0000000..bdc18b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.data.model
+
+/**
+ * Models an input method editor (IME).
+ *
+ * @see android.view.inputmethod.InputMethodInfo
+ */
+data class InputMethodModel(
+    /** A unique ID for this input method. */
+    val imeId: String,
+    /** The subtypes of this IME (may be empty). */
+    val subtypes: List<Subtype>,
+) {
+    /**
+     * A Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard), and is
+     * used for IME switch and settings.
+     *
+     * @see android.view.inputmethod.InputMethodSubtype
+     */
+    data class Subtype(
+        /** A unique ID for this IME subtype. */
+        val subtypeId: Int,
+        /**
+         * Whether this subtype is auxiliary. An auxiliary subtype will not be shown in the list of
+         * enabled IMEs for choosing the current IME in Settings, but it will be shown in the list
+         * of IMEs in the IME switcher to allow the user to tentatively switch to this subtype while
+         * an IME is shown.
+         *
+         * The intent of this flag is to allow for IMEs that are invoked in a one-shot way as
+         * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice
+         * input).
+         */
+        val isAuxiliary: Boolean,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
new file mode 100644
index 0000000..5f316c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.data.repository
+
+import android.annotation.SuppressLint
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/** Provides access to input-method related application state in the bouncer. */
+interface InputMethodRepository {
+    /**
+     * Creates and returns a new `Flow` of installed input methods that are enabled for the
+     * specified user.
+     *
+     * @param fetchSubtypes Whether to fetch the IME Subtypes as well (requires an additional IPC
+     *   call for each IME, avoid if not needed).
+     * @see InputMethodManager.getEnabledInputMethodListAsUser
+     */
+    suspend fun enabledInputMethods(userId: Int, fetchSubtypes: Boolean): Flow<InputMethodModel>
+
+    /** Returns enabled subtypes for the currently selected input method. */
+    suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype>
+
+    /**
+     * Shows the system's input method picker dialog.
+     *
+     * @param displayId The display ID on which to show the dialog.
+     * @param showAuxiliarySubtypes Whether to show auxiliary input method subtypes in the list of
+     *   enabled IMEs.
+     * @see InputMethodManager.showInputMethodPickerFromSystem
+     */
+    suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean)
+}
+
+@SysUISingleton
+class InputMethodRepositoryImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val inputMethodManager: InputMethodManager,
+) : InputMethodRepository {
+
+    override suspend fun enabledInputMethods(
+        userId: Int,
+        fetchSubtypes: Boolean
+    ): Flow<InputMethodModel> {
+        return withContext(backgroundDispatcher) {
+                inputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(userId))
+            }
+            .asFlow()
+            .map { inputMethodInfo ->
+                InputMethodModel(
+                    imeId = inputMethodInfo.id,
+                    subtypes =
+                        if (fetchSubtypes) {
+                            enabledInputMethodSubtypes(
+                                inputMethodInfo,
+                                allowsImplicitlyEnabledSubtypes = true
+                            )
+                        } else {
+                            listOf()
+                        }
+                )
+            }
+    }
+
+    override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> {
+        return enabledInputMethodSubtypes(
+            inputMethodInfo = null, // Fetch subtypes for the currently-selected IME.
+            allowsImplicitlyEnabledSubtypes = false
+        )
+    }
+
+    @SuppressLint("MissingPermission")
+    override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        withContext(backgroundDispatcher) {
+            inputMethodManager.showInputMethodPickerFromSystem(showAuxiliarySubtypes, displayId)
+        }
+    }
+
+    /**
+     * Returns a list of enabled input method subtypes for the specified input method info.
+     *
+     * @param inputMethodInfo The [InputMethodInfo] whose subtypes list will be returned. If `null`,
+     *   returns enabled subtypes for the currently selected [InputMethodInfo].
+     * @param allowsImplicitlyEnabledSubtypes Whether to allow to return the implicitly enabled
+     *   subtypes. If an input method info doesn't have enabled subtypes, the framework will
+     *   implicitly enable subtypes according to the current system language.
+     * @see InputMethodManager.getEnabledInputMethodSubtypeList
+     */
+    private suspend fun enabledInputMethodSubtypes(
+        inputMethodInfo: InputMethodInfo?,
+        allowsImplicitlyEnabledSubtypes: Boolean
+    ): List<InputMethodModel.Subtype> {
+        return withContext(backgroundDispatcher) {
+                inputMethodManager.getEnabledInputMethodSubtypeList(
+                    inputMethodInfo,
+                    allowsImplicitlyEnabledSubtypes
+                )
+            }
+            .map {
+                InputMethodModel.Subtype(
+                    subtypeId = it.subtypeId,
+                    isAuxiliary = it.isAuxiliary,
+                )
+            }
+    }
+}
+
+@Module
+interface InputMethodRepositoryModule {
+    @Binds fun repository(impl: InputMethodRepositoryImpl): InputMethodRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt
new file mode 100644
index 0000000..c54aa7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.repository.InputMethodRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+
+/** Hosts application business logic related to input methods (e.g. software keyboard). */
+@SysUISingleton
+class InputMethodInteractor
+@Inject
+constructor(
+    private val repository: InputMethodRepository,
+) {
+    /**
+     * Returns whether there are multiple enabled input methods to choose from for password input.
+     *
+     * Method adapted from `com.android.inputmethod.latin.Utils`.
+     */
+    suspend fun hasMultipleEnabledImesOrSubtypes(userId: Int): Boolean {
+        // Count IMEs that either have no subtypes, or have at least one non-auxiliary subtype.
+        val matchingInputMethods =
+            repository
+                .enabledInputMethods(userId, fetchSubtypes = true)
+                .filter { ime -> ime.subtypes.isEmpty() || ime.subtypes.any { !it.isAuxiliary } }
+                .take(2) // Short-circuit if we find at least 2 matching IMEs.
+
+        return matchingInputMethods.count() > 1 || repository.selectedInputMethodSubtypes().size > 1
+    }
+
+    /** Shows the system's input method picker dialog. */
+    suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        repository.showInputMethodPicker(displayId, showAuxiliarySubtypes)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
index 26e83a3..f16a3bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
@@ -17,7 +17,7 @@
 
 package com.android.systemui.keyboard
 
-import com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag
+import android.hardware.input.InputSettings
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
@@ -40,7 +40,7 @@
         if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
             keyboardBacklightDialogCoordinator.get().startListening()
         }
-        if (keyboardA11yStickyKeysFlag()) {
+        if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
             stickyKeysIndicatorCoordinator.get().startListening()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
index b68551b..c3a618d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
@@ -58,7 +58,6 @@
                     dialog = null
                 } else if (dialog == null) {
                     dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply {
-                        setCanceledOnTouchOutside(false)
                         window?.setAttributes()
                         show()
                     }
@@ -70,6 +69,7 @@
     private fun Window.setAttributes() {
         setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
         addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+        addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
         clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
         setGravity(Gravity.TOP or Gravity.END)
         attributes = WindowManager.LayoutParams().apply {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index f10b87e..b5f9c69 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -85,10 +85,10 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.power.shared.model.ScreenPowerState;
 import com.android.systemui.settings.DisplayTracker;
+import com.android.wm.shell.shared.CounterRotator;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.CounterRotator;
-import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
 import java.util.Map;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index afef875..7f43fac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener
 import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
@@ -86,6 +87,7 @@
     private val vibratorHelper: VibratorHelper,
     private val falsingManager: FalsingManager,
     private val aodAlphaViewModel: AodAlphaViewModel,
+    private val keyguardClockViewModel: KeyguardClockViewModel,
 ) : CoreStartable {
 
     private var rootViewHandle: DisposableHandle? = null
@@ -113,7 +115,11 @@
         initializeViews()
 
         if (!SceneContainerFlag.isEnabled) {
-            KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+            KeyguardBlueprintViewBinder.bind(
+                keyguardRootView,
+                keyguardBlueprintViewModel,
+                keyguardClockViewModel
+            )
         }
         keyguardBlueprintCommandListener.start()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 13e3835..e16f8dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -51,7 +51,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
-import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule;
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
@@ -104,7 +104,7 @@
             FalsingModule.class,
             KeyguardDataQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
-            KeyguardFaceAuthModule.class,
+            DeviceEntryFaceAuthModule.class,
             KeyguardDisplayModule.class,
             StartKeyguardTransitionModule.class,
             ResourceTrimmerModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
index fede479..4cd544f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.ui.binder.LiftToRunFaceAuthBinder
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import dagger.Binds
@@ -32,7 +33,7 @@
 import dagger.multibindings.IntoMap
 
 @Module
-interface KeyguardFaceAuthModule {
+interface DeviceEntryFaceAuthModule {
     @Binds
     fun deviceEntryFaceAuthRepository(
         impl: DeviceEntryFaceAuthRepositoryImpl
@@ -41,13 +42,20 @@
     @Binds
     @IntoMap
     @ClassKey(SystemUIDeviceEntryFaceAuthInteractor::class)
-    fun bind(impl: SystemUIDeviceEntryFaceAuthInteractor): CoreStartable
+    fun bindSystemUIDeviceEntryFaceAuthInteractor(
+        impl: SystemUIDeviceEntryFaceAuthInteractor
+    ): CoreStartable
 
     @Binds
     fun keyguardFaceAuthInteractor(
         impl: SystemUIDeviceEntryFaceAuthInteractor
     ): DeviceEntryFaceAuthInteractor
 
+    @Binds
+    @IntoMap
+    @ClassKey(LiftToRunFaceAuthBinder::class)
+    fun bindLiftToRunFaceAuthBinder(impl: LiftToRunFaceAuthBinder): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index 48b6634..9381830 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -23,6 +23,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
 import java.io.PrintWriter
 import java.util.TreeMap
 import javax.inject.Inject
@@ -54,6 +56,8 @@
         TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
     val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
     val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
+    val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
+        MutableSharedFlow(extraBufferCapacity = 1)
     val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange
 
     /**
@@ -103,7 +107,12 @@
 
     /** Re-emits the last emitted blueprint value if possible. */
     fun refreshBlueprint() {
+        refreshBlueprintWithTransition(DefaultTransition)
+    }
+
+    fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
         refreshBluePrint.tryEmit(Unit)
+        refreshBlueprintTransition.tryEmit(type)
     }
 
     /** Prints all available blueprints to the PrintWriter. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 9d38be9..71d941a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -64,6 +65,7 @@
         listenForHubToAlternateBouncer()
         listenForHubToOccluded()
         listenForHubToGone()
+        listenForHubToDreaming()
     }
 
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -131,6 +133,23 @@
         }
     }
 
+    private fun listenForHubToDreaming() {
+        val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+        scope.launch("$TAG#listenForHubToDreaming") {
+            keyguardInteractor.isAbleToDream
+                .sampleMultiple(startedKeyguardTransitionStep, finishedKeyguardState)
+                .collect { (isAbleToDream, lastStartedTransition, finishedKeyguardState) ->
+                    val isOnHub = finishedKeyguardState == KeyguardState.GLANCEABLE_HUB
+                    val isTransitionInterruptible =
+                        lastStartedTransition.to == KeyguardState.GLANCEABLE_HUB &&
+                            !invalidFromStates.contains(lastStartedTransition.from)
+                    if (isAbleToDream && (isOnHub || isTransitionInterruptible)) {
+                        startTransitionTo(KeyguardState.DREAMING)
+                    }
+                }
+        }
+    }
+
     private fun listenForHubToOccluded() {
         scope.launch {
             keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 3965648..deb70b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -405,7 +405,9 @@
             interpolator = Interpolators.LINEAR
             duration =
                 when (toState) {
-                    KeyguardState.DREAMING -> TO_DREAMING_DURATION
+                    // Adds 100ms to the overall delay to workaround legacy setOccluded calls
+                    // being delayed in KeyguardViewMediator
+                    KeyguardState.DREAMING -> TO_DREAMING_DURATION + 100.milliseconds
                     KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
                     KeyguardState.AOD -> TO_AOD_DURATION
                     KeyguardState.DOZING -> TO_DOZING_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index ba44e68..566e006 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -24,12 +24,13 @@
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 
@@ -48,13 +49,15 @@
      *
      * This flow can also emit the same blueprint value if refreshBlueprint is emitted.
      */
-    val blueprint: Flow<KeyguardBlueprint> =
-        merge(
-            keyguardBlueprintRepository.blueprint,
-            keyguardBlueprintRepository.refreshBluePrint.map {
-                keyguardBlueprintRepository.blueprint.value
-            }
-        )
+    val blueprint: Flow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint
+
+    val blueprintWithTransition =
+        combine(
+            keyguardBlueprintRepository.refreshBluePrint,
+            keyguardBlueprintRepository.refreshBlueprintTransition
+        ) { _, source ->
+            source
+        }
 
     init {
         applicationScope.launch {
@@ -107,6 +110,10 @@
         keyguardBlueprintRepository.refreshBlueprint()
     }
 
+    fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
+        keyguardBlueprintRepository.refreshBlueprintWithTransition(type)
+    }
+
     fun getCurrentBlueprint(): KeyguardBlueprint {
         return keyguardBlueprintRepository.blueprint.value
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
new file mode 100644
index 0000000..a0f9be6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** FROM -> TO keyguard transition. null values are allowed to signify FROM -> *, or * -> TO */
+data class Edge(
+    val from: KeyguardState?,
+    val to: KeyguardState?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 4abda74..00b7989 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
@@ -174,11 +175,6 @@
         }
     }
 
-    data class Edge(
-        val from: KeyguardState?,
-        val to: KeyguardState?,
-    )
-
     data class StateToValue(
         val transitionState: TransitionState,
         val value: Float?,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 703bb87..873cc84 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -19,6 +19,7 @@
 
 import android.annotation.SuppressLint
 import android.content.res.ColorStateList
+import android.util.StateSet
 import android.view.HapticFeedbackConstants
 import android.view.View
 import androidx.lifecycle.Lifecycle
@@ -113,6 +114,8 @@
 
         fgIconView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Start with an empty state
+                fgIconView.setImageState(StateSet.NOTHING, /* merge */ false)
                 launch {
                     fgViewModel.viewModel.collect { viewModel ->
                         fgIconView.setImageState(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 8d6493f..404046b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -26,7 +26,10 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import kotlinx.coroutines.launch
 
@@ -34,7 +37,11 @@
     companion object {
         private const val TAG = "KeyguardBlueprintViewBinder"
 
-        fun bind(constraintLayout: ConstraintLayout, viewModel: KeyguardBlueprintViewModel) {
+        fun bind(
+            constraintLayout: ConstraintLayout,
+            viewModel: KeyguardBlueprintViewModel,
+            clockViewModel: KeyguardClockViewModel
+        ) {
             constraintLayout.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch {
@@ -42,6 +49,7 @@
                             val prevBluePrint = viewModel.currentBluePrint
                             Trace.beginSection("KeyguardBlueprint#applyBlueprint")
                             Log.d(TAG, "applying blueprint: $blueprint")
+                            TransitionManager.endTransitions(constraintLayout)
 
                             val cs =
                                 ConstraintSet().apply {
@@ -54,11 +62,28 @@
                                 }
 
                             // Apply transition.
-                            if (!keyguardBottomAreaRefactor() && prevBluePrint != null &&
-                                prevBluePrint != blueprint) {
+                            if (
+                                !keyguardBottomAreaRefactor() &&
+                                    prevBluePrint != null &&
+                                    prevBluePrint != blueprint
+                            ) {
                                 TransitionManager.beginDelayedTransition(
                                     constraintLayout,
-                                    BaseBlueprintTransition()
+                                    BaseBlueprintTransition(clockViewModel)
+                                        .addTransition(
+                                            IntraBlueprintTransition(
+                                                IntraBlueprintTransitionType.NoTransition,
+                                                clockViewModel
+                                            )
+                                        )
+                                )
+                            } else {
+                                TransitionManager.beginDelayedTransition(
+                                    constraintLayout,
+                                    IntraBlueprintTransition(
+                                        IntraBlueprintTransitionType.NoTransition,
+                                        clockViewModel
+                                    )
                                 )
                             }
 
@@ -71,6 +96,25 @@
                             Trace.endSection()
                         }
                     }
+
+                    launch {
+                        viewModel.blueprintWithTransition.collect { source ->
+                            TransitionManager.endTransitions(constraintLayout)
+
+                            val cs =
+                                ConstraintSet().apply {
+                                    clone(constraintLayout)
+                                    viewModel.currentBluePrint?.applyConstraints(this)
+                                }
+
+                            TransitionManager.beginDelayedTransition(
+                                constraintLayout,
+                                IntraBlueprintTransition(source, clockViewModel)
+                            )
+                            cs.applyTo(constraintLayout)
+                            Trace.endSection()
+                        }
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 3c3ebdf..f0e89f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -16,34 +16,30 @@
 
 package com.android.systemui.keyguard.ui.binder
 
-import android.animation.Animator
-import android.animation.ValueAnimator
-import android.transition.Transition
 import android.transition.TransitionManager
 import android.transition.TransitionSet
-import android.transition.TransitionValues
-import android.view.ViewGroup
+import android.util.Log
+import android.view.View.INVISIBLE
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.app.animation.Interpolators
 import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.keyguard.KeyguardClockSwitch.SMALL
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.res.R
+import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID
 import kotlinx.coroutines.launch
 
-private const val KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS = 1000L
-
 object KeyguardClockViewBinder {
     @JvmStatic
     fun bind(
@@ -67,32 +63,56 @@
                         viewModel.clock = currentClock
                         addClockViews(currentClock, keyguardRootView)
                         updateBurnInLayer(keyguardRootView, viewModel)
-                        blueprintInteractor.refreshBlueprint()
+                        applyConstraints(clockSection, keyguardRootView, true)
                     }
                 }
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.clockSize.collect {
                         updateBurnInLayer(keyguardRootView, viewModel)
-                        blueprintInteractor.refreshBlueprint()
+                        blueprintInteractor.refreshBlueprintWithTransition(
+                            IntraBlueprintTransitionType.ClockSize
+                        )
                     }
                 }
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
-                    viewModel.clockShouldBeCentered.collect {
+                    viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
+                        Log.d(
+                            "ClockViewBinder",
+                            "Sherry clockShouldBeCentered $clockShouldBeCentered"
+                        )
                         viewModel.clock?.let {
-                            if (it.largeClock.config.hasCustomPositionUpdatedAnimation) {
-                                playClockCenteringAnimation(clockSection, keyguardRootView, it)
+                            // Weather clock also has hasCustomPositionUpdatedAnimation as true
+                            // TODO(b/323020908): remove ID check
+                            if (
+                                it.largeClock.config.hasCustomPositionUpdatedAnimation &&
+                                    it.config.id == DEFAULT_CLOCK_ID
+                            ) {
+                                blueprintInteractor.refreshBlueprintWithTransition(
+                                    IntraBlueprintTransitionType.DefaultClockStepping
+                                )
                             } else {
-                                blueprintInteractor.refreshBlueprint()
+                                blueprintInteractor.refreshBlueprintWithTransition(
+                                    IntraBlueprintTransitionType.DefaultTransition
+                                )
                             }
                         }
                     }
                 }
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
-                    viewModel.isAodIconsVisible.collect {
-                        applyConstraints(clockSection, keyguardRootView, true)
+                    viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
+                        viewModel.clock?.let {
+                            // Weather clock also has hasCustomPositionUpdatedAnimation as true
+                            if (
+                                viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
+                            ) {
+                                blueprintInteractor.refreshBlueprintWithTransition(
+                                    IntraBlueprintTransitionType.DefaultTransition
+                                )
+                            }
+                        }
                     }
                 }
             }
@@ -155,7 +175,9 @@
             }
             // small clock should either be a single view or container with id
             // `lockscreen_clock_view`
-            clock.smallClock.layout.views.forEach { rootView.addView(it) }
+            clock.smallClock.layout.views.forEach {
+                rootView.addView(it).apply { it.visibility = INVISIBLE }
+            }
             clock.largeClock.layout.views.forEach { rootView.addView(it) }
         }
     }
@@ -173,74 +195,4 @@
         }
         constraintSet.applyTo(rootView)
     }
-
-    private fun playClockCenteringAnimation(
-        clockSection: ClockSection,
-        keyguardRootView: ConstraintLayout,
-        clock: ClockController,
-    ) {
-        // Find the clock, so we can exclude it from this transition.
-        val clockView = clock.largeClock.view
-        val set = TransitionSet()
-        val adapter = SplitShadeTransitionAdapter(clock)
-        adapter.setInterpolator(Interpolators.LINEAR)
-        adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS)
-        adapter.addTarget(clockView)
-        set.addTransition(adapter)
-        applyConstraints(clockSection, keyguardRootView, true, set)
-    }
-
-    internal class SplitShadeTransitionAdapter
-    @VisibleForTesting
-    constructor(private val clock: ClockController) : Transition() {
-        private fun captureValues(transitionValues: TransitionValues) {
-            transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
-            val locationInWindowTmp = IntArray(2)
-            transitionValues.view.getLocationInWindow(locationInWindowTmp)
-            transitionValues.values[PROP_X_IN_WINDOW] = locationInWindowTmp[0]
-        }
-
-        override fun captureEndValues(transitionValues: TransitionValues) {
-            captureValues(transitionValues)
-        }
-
-        override fun captureStartValues(transitionValues: TransitionValues) {
-            captureValues(transitionValues)
-        }
-
-        override fun createAnimator(
-            sceneRoot: ViewGroup,
-            startValues: TransitionValues?,
-            endValues: TransitionValues?
-        ): Animator? {
-            if (startValues == null || endValues == null) {
-                return null
-            }
-            val anim = ValueAnimator.ofFloat(0f, 1f)
-            val fromLeft = startValues.values[PROP_BOUNDS_LEFT] as Int
-            val fromWindowX = startValues.values[PROP_X_IN_WINDOW] as Int
-            val toWindowX = endValues.values[PROP_X_IN_WINDOW] as Int
-            // Using windowX, to determine direction, instead of left, as in RTL the difference of
-            // toLeft - fromLeft is always positive, even when moving left.
-            val direction = if (toWindowX - fromWindowX > 0) 1 else -1
-            anim.addUpdateListener { animation: ValueAnimator ->
-                clock.largeClock.animations.onPositionUpdated(
-                    fromLeft,
-                    direction,
-                    animation.animatedFraction
-                )
-            }
-            return anim
-        }
-
-        override fun getTransitionProperties(): Array<String> {
-            return TRANSITION_PROPERTIES
-        }
-
-        companion object {
-            private const val PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"
-            private const val PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"
-            private val TRANSITION_PROPERTIES = arrayOf(PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW)
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 10392e3..08a2b9c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -49,14 +49,15 @@
                             clockViewModel,
                             smartspaceViewModel
                         )
-                        blueprintInteractor.refreshBlueprint()
+                        blueprintInteractor.refreshBlueprintWithTransition()
                     }
                 }
 
                 launch {
+                    if (!migrateClocksToBlueprint()) return@launch
                     smartspaceViewModel.bcSmartspaceVisibility.collect {
                         updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
-                        blueprintInteractor.refreshBlueprint()
+                        blueprintInteractor.refreshBlueprintWithTransition()
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index fd530b7..9c9df80 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -19,30 +19,32 @@
 import android.animation.Animator
 import android.animation.ObjectAnimator
 import android.transition.ChangeBounds
+import android.transition.Transition
 import android.transition.TransitionSet
 import android.transition.TransitionValues
 import android.transition.Visibility
 import android.view.View
 import android.view.ViewGroup
 import androidx.constraintlayout.helper.widget.Layer
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
-import com.android.systemui.res.R
 
-class BaseBlueprintTransition : TransitionSet() {
+class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : TransitionSet() {
     init {
         ordering = ORDERING_SEQUENTIAL
         addTransition(AlphaOutVisibility())
             .addTransition(ChangeBounds())
             .addTransition(AlphaInVisibility())
         excludeTarget(Layer::class.java, /* exclude= */ true)
-        excludeClockAndSmartspaceViews()
+        excludeClockAndSmartspaceViews(this)
     }
 
-    private fun excludeClockAndSmartspaceViews() {
-        excludeTarget(R.id.lockscreen_clock_view, true)
-        excludeTarget(R.id.lockscreen_clock_view_large, true)
-        excludeTarget(SmartspaceView::class.java, true)
-        // TODO(b/319468190): need to exclude views from large weather clock
+    private fun excludeClockAndSmartspaceViews(transition: Transition) {
+        transition.excludeTarget(SmartspaceView::class.java, true)
+        clockViewModel.clock?.let { clock ->
+            clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
+            clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
+        }
     }
 
     class AlphaOutVisibility : Visibility() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
new file mode 100644
index 0000000..524aa1a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.blueprints.transitions
+
+import android.transition.TransitionSet
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.DefaultClockSteppingTransition
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+
+enum class IntraBlueprintTransitionType {
+    ClockSize,
+    ClockCenter,
+    DefaultClockStepping,
+    DefaultTransition,
+    AodNotifIconsTransition,
+    // When transition between blueprint, we don't need any duration or interpolator but we need
+    // all elements go to correct state
+    NoTransition,
+}
+
+class IntraBlueprintTransition(
+    type: IntraBlueprintTransitionType,
+    clockViewModel: KeyguardClockViewModel
+) : TransitionSet() {
+    init {
+        ordering = ORDERING_TOGETHER
+        if (type == IntraBlueprintTransitionType.DefaultClockStepping)
+            addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+        addTransition(ClockSizeTransition(type, clockViewModel))
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index fe4f07d..b1178f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -166,10 +166,7 @@
                     context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
             largeClockTopMargin += getDimen(DATE_WEATHER_VIEW_HEIGHT)
             largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT)
-            if (!keyguardClockViewModel.useLargeClock) {
-                largeClockTopMargin -=
-                    context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
-            }
+
             connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
             constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
             constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 400d0dc..a651c10 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -87,7 +87,8 @@
             return
         }
         // This moves the existing NSSL view to a different parent, as the controller is a
-        // singleton and recreating it has other bad side effects
+        // singleton and recreating it has other bad side effects.
+        // In the SceneContainer, this is done by the NotificationSection composable.
         notificationPanelView.findViewById<View?>(R.id.notification_stack_scroller)?.let {
             (it.parent as ViewGroup).removeView(it)
             sharedNotificationContainer.addNotificationStackScrollLayout(it)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
new file mode 100644
index 0000000..99565b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections.transitions
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.transition.ChangeBounds
+import android.transition.TransitionSet
+import android.transition.TransitionValues
+import android.transition.Visibility
+import android.view.View
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
+
+const val CLOCK_OUT_MILLIS = 133L
+const val CLOCK_IN_MILLIS = 167L
+val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN
+const val CLOCK_IN_START_DELAY_MILLIS = 133L
+val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR
+
+class ClockSizeTransition(
+    val type: IntraBlueprintTransitionType,
+    clockViewModel: KeyguardClockViewModel
+) : TransitionSet() {
+    init {
+        ordering = ORDERING_TOGETHER
+        addTransition(ClockOutTransition(clockViewModel, type))
+        addTransition(ClockInTransition(clockViewModel, type))
+        addTransition(SmartspaceChangeBounds(clockViewModel, type))
+        addTransition(ClockInChangeBounds(clockViewModel, type))
+        addTransition(ClockOutChangeBounds(clockViewModel, type))
+    }
+
+    class ClockInTransition(viewModel: KeyguardClockViewModel, type: IntraBlueprintTransitionType) :
+        Visibility() {
+        init {
+            mode = MODE_IN
+            if (type != IntraBlueprintTransitionType.NoTransition) {
+                duration = CLOCK_IN_MILLIS
+                startDelay = CLOCK_IN_START_DELAY_MILLIS
+                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+            } else {
+                duration = 0
+                startDelay = 0
+            }
+
+            addTarget(sharedR.id.bc_smartspace_view)
+            addTarget(sharedR.id.date_smartspace_view)
+            addTarget(sharedR.id.weather_smartspace_view)
+            if (viewModel.useLargeClock) {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            } else {
+                addTarget(R.id.lockscreen_clock_view)
+            }
+        }
+
+        override fun onAppear(
+            sceneRoot: ViewGroup?,
+            view: View,
+            startValues: TransitionValues?,
+            endValues: TransitionValues?
+        ): Animator {
+            return ObjectAnimator.ofFloat(view, "alpha", 1f).also {
+                it.duration = duration
+                it.startDelay = startDelay
+                it.interpolator = interpolator
+                it.addUpdateListener { view.alpha = it.animatedValue as Float }
+                it.start()
+            }
+        }
+    }
+
+    class ClockOutTransition(
+        viewModel: KeyguardClockViewModel,
+        type: IntraBlueprintTransitionType
+    ) : Visibility() {
+        init {
+            mode = MODE_OUT
+            if (type != IntraBlueprintTransitionType.NoTransition) {
+                duration = CLOCK_OUT_MILLIS
+                interpolator = CLOCK_OUT_INTERPOLATOR
+            } else {
+                duration = 0
+            }
+
+            addTarget(sharedR.id.bc_smartspace_view)
+            addTarget(sharedR.id.date_smartspace_view)
+            addTarget(sharedR.id.weather_smartspace_view)
+            if (viewModel.useLargeClock) {
+                addTarget(R.id.lockscreen_clock_view)
+            } else {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            }
+        }
+
+        override fun onDisappear(
+            sceneRoot: ViewGroup?,
+            view: View,
+            startValues: TransitionValues?,
+            endValues: TransitionValues?
+        ): Animator {
+            return ObjectAnimator.ofFloat(view, "alpha", 0f).also {
+                it.duration = duration
+                it.interpolator = interpolator
+                it.addUpdateListener { view.alpha = it.animatedValue as Float }
+                it.start()
+            }
+        }
+    }
+
+    class ClockInChangeBounds(
+        viewModel: KeyguardClockViewModel,
+        type: IntraBlueprintTransitionType
+    ) : ChangeBounds() {
+        init {
+            if (type != IntraBlueprintTransitionType.NoTransition) {
+                duration = CLOCK_IN_MILLIS
+                startDelay = CLOCK_IN_START_DELAY_MILLIS
+                interpolator = CLOCK_IN_INTERPOLATOR
+            } else {
+                duration = 0
+                startDelay = 0
+            }
+
+            if (viewModel.useLargeClock) {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            } else {
+                addTarget(R.id.lockscreen_clock_view)
+            }
+        }
+    }
+
+    class ClockOutChangeBounds(
+        viewModel: KeyguardClockViewModel,
+        type: IntraBlueprintTransitionType
+    ) : ChangeBounds() {
+        init {
+            if (type != IntraBlueprintTransitionType.NoTransition) {
+                duration = CLOCK_OUT_MILLIS
+                interpolator = CLOCK_OUT_INTERPOLATOR
+            } else {
+                duration = 0
+            }
+            if (viewModel.useLargeClock) {
+                addTarget(R.id.lockscreen_clock_view)
+            } else {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            }
+        }
+    }
+
+    class SmartspaceChangeBounds(
+        viewModel: KeyguardClockViewModel,
+        val type: IntraBlueprintTransitionType = IntraBlueprintTransitionType.DefaultTransition
+    ) : ChangeBounds() {
+        init {
+            if (type != IntraBlueprintTransitionType.NoTransition) {
+                duration =
+                    if (viewModel.useLargeClock) {
+                        STATUS_AREA_MOVE_UP_MILLIS
+                    } else {
+                        STATUS_AREA_MOVE_DOWN_MILLIS
+                    }
+                interpolator = Interpolators.EMPHASIZED
+            } else {
+                duration = 0
+            }
+            addTarget(sharedR.id.date_smartspace_view)
+            addTarget(sharedR.id.weather_smartspace_view)
+            addTarget(sharedR.id.bc_smartspace_view)
+        }
+
+        companion object {
+            const val STATUS_AREA_MOVE_UP_MILLIS = 967L
+            const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
new file mode 100644
index 0000000..c35dad7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections.transitions
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators
+import com.android.systemui.plugins.clocks.ClockController
+
+class DefaultClockSteppingTransition(private val clock: ClockController) : Transition() {
+    init {
+        interpolator = Interpolators.LINEAR
+        duration = KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS
+        addTarget(clock.largeClock.view)
+    }
+    private fun captureValues(transitionValues: TransitionValues) {
+        transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
+        val locationInWindowTmp = IntArray(2)
+        transitionValues.view.getLocationInWindow(locationInWindowTmp)
+        transitionValues.values[PROP_X_IN_WINDOW] = locationInWindowTmp[0]
+    }
+
+    override fun captureEndValues(transitionValues: TransitionValues) {
+        captureValues(transitionValues)
+    }
+
+    override fun captureStartValues(transitionValues: TransitionValues) {
+        captureValues(transitionValues)
+    }
+
+    override fun createAnimator(
+        sceneRoot: ViewGroup,
+        startValues: TransitionValues?,
+        endValues: TransitionValues?
+    ): Animator? {
+        if (startValues == null || endValues == null) {
+            return null
+        }
+        val anim = ValueAnimator.ofFloat(0f, 1f)
+        val fromLeft = startValues.values[PROP_BOUNDS_LEFT] as Int
+        val fromWindowX = startValues.values[PROP_X_IN_WINDOW] as Int
+        val toWindowX = endValues.values[PROP_X_IN_WINDOW] as Int
+        // Using windowX, to determine direction, instead of left, as in RTL the difference of
+        // toLeft - fromLeft is always positive, even when moving left.
+        val direction = if (toWindowX - fromWindowX > 0) 1 else -1
+        anim.addUpdateListener { animation: ValueAnimator ->
+            clock.largeClock.animations.onPositionUpdated(
+                fromLeft,
+                direction,
+                animation.animatedFraction
+            )
+        }
+        return anim
+    }
+
+    override fun getTransitionProperties(): Array<String> {
+        return TRANSITION_PROPERTIES
+    }
+
+    companion object {
+        private const val PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"
+        private const val PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"
+        private val TRANSITION_PROPERTIES = arrayOf(PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW)
+        private const val KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS = 1000L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
index 310ec95..ad6a36c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlin.math.roundToInt
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -74,7 +75,18 @@
                 isTransitionToAod && isUdfps
             }
             .distinctUntilChanged()
-    private val padding: Flow<Int> = udfpsOverlayInteractor.iconPadding
+
+    private val padding: Flow<Int> =
+        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported ->
+            if (udfpsSupported) {
+                udfpsOverlayInteractor.iconPadding
+            } else {
+                configurationInteractor.scaleForResolution.map { scale ->
+                    (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
+                        .roundToInt()
+                }
+            }
+        }
 
     val viewModel: Flow<ForegroundIconViewModel> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index e2bfc36..d22856b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -26,4 +26,5 @@
 constructor(keyguardBlueprintInteractor: KeyguardBlueprintInteractor) {
     var currentBluePrint: KeyguardBlueprint? = null
     val blueprint = keyguardBlueprintInteractor.blueprint
+    val blueprintWithTransition = keyguardBlueprintInteractor.blueprintWithTransition
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index a3029b2..23ee00d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -146,7 +146,7 @@
                 null,
                 UserHandle.ALL
             )
-            userTracker.addCallback(userTrackerCallback, backgroundExecutor)
+            userTracker.addCallback(userTrackerCallback, mainExecutor)
             loadSavedComponents()
         }
     }
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 523414c..c33ab12 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
@@ -586,7 +586,7 @@
         coroutineScope.launch {
             communalInteractor.isCommunalShowing.collect { value ->
                 isCommunalShowing = value
-                updateDesiredLocation(forceNoAnimation = true)
+                updateDesiredLocation()
             }
         }
     }
@@ -1149,13 +1149,17 @@
             when {
                 mediaFlags.isSceneContainerEnabled() -> desiredLocation
                 dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
+
+                // UMO should show in communal unless the shade is expanding or visible.
+                isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB
                 (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
                 qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
                 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
                 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-                // TODO(b/311234666): revisit logic once interactions between the hub and
-                //  shade/keyguard state are finalized
-                isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB
+
+                // Communal does not have its own StatusBarState so it should always have higher
+                // priority for the UMO over the lockscreen.
+                isCommunalShowing -> LOCATION_COMMUNAL_HUB
                 onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
                 else -> LOCATION_QQS
             }
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 21de185..958ace35 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -26,8 +26,6 @@
 import android.database.ContentObserver;
 import android.os.BatteryManager;
 import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.IThermalEventListener;
 import android.os.IThermalService;
 import android.os.PowerManager;
@@ -97,7 +95,6 @@
     private Future mLastShowWarningTask;
     private boolean mEnableSkinTemperatureWarning;
     private boolean mEnableUsbTemperatureAlarm;
-    private final HandlerThread mHandlerThread;
 
     private int mLowBatteryAlertCloseLevel;
     private final int[] mLowBatteryReminderLevels = new int[2];
@@ -170,8 +167,6 @@
         mPowerManager = powerManager;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mUserTracker = userTracker;
-        mHandlerThread = new HandlerThread("PowerUI");
-        mHandlerThread.start();
     }
 
     public void start() {
@@ -190,8 +185,7 @@
                 false, obs, UserHandle.USER_ALL);
         updateBatteryWarningLevels();
         mReceiver.init();
-        mUserTracker.addCallback(mUserChangedCallback,
-                    new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
         mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
 
         // Check to see if we need to let the user know that the phone previously shut down due
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index ac0bd29..1c37510f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -77,6 +77,7 @@
     private Consumer<Boolean> mMediaVisibilityChangedListener;
     @Orientation
     private int mLastOrientation;
+    private int mLastScreenLayout;
     private String mCachedSpecs = "";
     @Nullable
     private QSTileRevealController mQsTileRevealController;
@@ -93,15 +94,19 @@
                 public void onConfigurationChange(Configuration newConfig) {
                     final boolean previousSplitShadeState = mShouldUseSplitNotificationShade;
                     final int previousOrientation = mLastOrientation;
+                    final int previousScreenLayout = mLastScreenLayout;
                     mShouldUseSplitNotificationShade = mSplitShadeStateController
                             .shouldUseSplitNotificationShade(getResources());
                     mLastOrientation = newConfig.orientation;
+                    mLastScreenLayout = newConfig.screenLayout;
 
                     mQSLogger.logOnConfigurationChanged(
                         /* oldOrientation= */ previousOrientation,
                         /* newOrientation= */ mLastOrientation,
                         /* oldShouldUseSplitShade= */ previousSplitShadeState,
                         /* newShouldUseSplitShade= */ mShouldUseSplitNotificationShade,
+                        /* oldScreenLayout= */ previousScreenLayout,
+                        /* newScreenLayout= */ mLastScreenLayout,
                         /* containerName= */ mView.getDumpableTag());
 
                     switchTileLayoutIfNeeded();
@@ -198,6 +203,7 @@
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
         setTiles();
         mLastOrientation = getResources().getConfiguration().orientation;
+        mLastScreenLayout = getResources().getConfiguration().screenLayout;
         mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag());
         switchTileLayout(true);
 
@@ -443,11 +449,13 @@
     }
 
     boolean shouldUseHorizontalLayout() {
-        if (mShouldUseSplitNotificationShade)  {
+        if (mShouldUseSplitNotificationShade) {
             return false;
         }
         return mUsingMediaPlayer && mMediaHost.getVisible()
-                && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE;
+                && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE
+                && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK)
+                == Configuration.SCREENLAYOUT_LONG_YES;
     }
 
     private void logTiles() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 38e7972..b515ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -19,6 +19,8 @@
 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
 import android.content.res.Configuration.ORIENTATION_PORTRAIT
 import android.content.res.Configuration.Orientation
+import android.content.res.Configuration.SCREENLAYOUT_LONG_NO
+import android.content.res.Configuration.SCREENLAYOUT_LONG_YES
 import android.service.quicksettings.Tile
 import android.view.View
 import com.android.systemui.log.ConstantStringsLogger
@@ -266,8 +268,10 @@
     fun logOnConfigurationChanged(
         @Orientation oldOrientation: Int,
         @Orientation newOrientation: Int,
-        newShouldUseSplitShade: Boolean,
         oldShouldUseSplitShade: Boolean,
+        newShouldUseSplitShade: Boolean,
+        oldScreenLayout: Int,
+        newScreenLayout: Int,
         containerName: String
     ) {
         configChangedBuffer.log(
@@ -277,6 +281,8 @@
                 str1 = containerName
                 int1 = oldOrientation
                 int2 = newOrientation
+                long1 = oldScreenLayout.toLong()
+                long2 = newScreenLayout.toLong()
                 bool1 = oldShouldUseSplitShade
                 bool2 = newShouldUseSplitShade
             },
@@ -284,6 +290,8 @@
                 "config change: " +
                     "$str1 orientation=${toOrientationString(int2)} " +
                     "(was ${toOrientationString(int1)}), " +
+                    "screen layout=${toScreenLayoutString(long1.toInt())} " +
+                    "(was ${toScreenLayoutString(long2.toInt())}), " +
                     "splitShade=$bool2 (was $bool1)"
             }
         )
@@ -370,3 +378,11 @@
         else -> "undefined"
     }
 }
+
+private inline fun toScreenLayoutString(screenLayout: Int): String {
+    return when (screenLayout) {
+        SCREENLAYOUT_LONG_YES -> "long"
+        SCREENLAYOUT_LONG_NO -> "notlong"
+        else -> "undefined"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index c5eeb2f..9fefcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -52,7 +52,7 @@
 import com.android.systemui.animation.LaunchableViewDelegate
 import com.android.systemui.plugins.qs.QSIconView
 import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.qs.QSTile.AdapterState
 import com.android.systemui.plugins.qs.QSTileView
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
@@ -506,13 +506,12 @@
             state.expandedAccessibilityClassName
         }
 
-        if (state is BooleanState) {
+        if (state is AdapterState) {
             val newState = state.value
             if (tileState != newState) {
                 tileState = newState
             }
         }
-        //
 
         // Labels
         if (!Objects.equals(label.text, state.label)) {
@@ -625,7 +624,7 @@
             customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
             customDrawableView.visibility = VISIBLE
             chevronView.visibility = GONE
-        } else if (state !is BooleanState || state.forceExpandIcon) {
+        } else if (state !is AdapterState || state.forceExpandIcon) {
             customDrawableView.setImageDrawable(null)
             customDrawableView.visibility = GONE
             chevronView.visibility = VISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt
new file mode 100644
index 0000000..26069c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [FontScalingTileModel] to [QSTileState]. */
+class FontScalingTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Resources.Theme,
+) : QSTileDataToStateMapper<FontScalingTileModel> {
+
+    override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            val icon =
+                Icon.Loaded(
+                    resources.getDrawable(
+                        R.drawable.ic_qs_font_scaling,
+                        theme,
+                    ),
+                    contentDescription = null
+                )
+            this.icon = { icon }
+            contentDescription = label
+            activationState = QSTileState.ActivationState.ACTIVE
+            sideViewIcon = QSTileState.SideViewIcon.Chevron
+            supportedActions =
+                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt
new file mode 100644
index 0000000..745e6a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Provides [FontScalingTileModel]. */
+class FontScalingTileDataInteractor @Inject constructor() :
+    QSTileDataInteractor<FontScalingTileModel> {
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<FontScalingTileModel> = flowOf(FontScalingTileModel)
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
new file mode 100644
index 0000000..b6f4afb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+/** Handles font scaling tile clicks. */
+class FontScalingTileUserActionInteractor
+@Inject
+constructor(
+    @Main private val coroutineContext: CoroutineContext,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+    private val fontScalingDialogDelegateProvider: Provider<FontScalingDialogDelegate>,
+    private val keyguardStateController: KeyguardStateController,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val activityStarter: ActivityStarter,
+) : QSTileUserActionInteractor<FontScalingTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<FontScalingTileModel>): Unit =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    // We animate from the touched view only if we are not on the keyguard
+                    val animateFromView: Boolean =
+                        action.view != null && !keyguardStateController.isShowing
+                    val runnable = Runnable {
+                        val dialog: SystemUIDialog =
+                            fontScalingDialogDelegateProvider.get().createDialog()
+                        if (animateFromView) {
+                            dialogLaunchAnimator.showFromView(
+                                dialog,
+                                action.view!!,
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                    INTERACTION_JANK_TAG
+                                )
+                            )
+                        } else {
+                            dialog.show()
+                        }
+                    }
+
+                    withContext(coroutineContext) {
+                        activityStarter.executeRunnableDismissingKeyguard(
+                            runnable,
+                            /* cancelAction= */ null,
+                            /* dismissShade= */ true,
+                            /* afterKeyguardGone= */ true,
+                            /* deferred= */ false
+                        )
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_TEXT_READING_SETTINGS)
+                    )
+                }
+            }
+        }
+    companion object {
+        private const val INTERACTION_JANK_TAG = "font_scaling"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt
new file mode 100644
index 0000000..76042df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling.domain.model
+
+/** FontScaling tile model. No data needed as the tile just opens a dialog. */
+data object FontScalingTileModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 4780a2e..5a389f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -227,7 +227,7 @@
         ): QSTile.State =
             // we have to use QSTile.BooleanState to support different side icons
             // which are bound to instanceof QSTile.BooleanState in QSTileView.
-            QSTile.BooleanState().apply {
+            QSTile.AdapterState().apply {
                 spec = config.tileSpec.spec
                 label = viewModelState.label
                 // This value is synthetic and doesn't have any meaning. It's only needed to satisfy
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4e89fbf..7d86a6a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -513,10 +513,6 @@
             notifySystemUiStateFlags(mSysUiState.getFlags());
 
             notifyConnectionChanged();
-            if (mDoneUserChanging != null) {
-                mDoneUserChanging.run();
-                mDoneUserChanging = null;
-            }
         }
 
         @Override
@@ -571,14 +567,11 @@
         }
     };
 
-    private Runnable mDoneUserChanging;
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
                 @Override
-                public void onUserChanging(int newUser, @NonNull Context userContext,
-                        @NonNull Runnable resultCallback) {
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
                     mConnectionBackoffAttempts = 0;
-                    mDoneUserChanging = resultCallback;
                     internalConnectToCurrentUser("User changed");
                 }
             };
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 246ccb1..37abc40 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printSection
 import com.android.systemui.util.println
@@ -55,6 +56,7 @@
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.launch
@@ -81,6 +83,7 @@
     private val simBouncerInteractor: Lazy<SimBouncerInteractor>,
     private val authenticationInteractor: Lazy<AuthenticationInteractor>,
     private val windowController: NotificationShadeWindowController,
+    private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
 ) : CoreStartable {
 
     override fun start() {
@@ -112,26 +115,33 @@
     private fun hydrateVisibility() {
         applicationScope.launch {
             // TODO(b/296114544): Combine with some global hun state to make it visible!
-            sceneInteractor.transitionState
-                .mapNotNull { state ->
-                    when (state) {
-                        is ObservableTransitionState.Idle -> {
-                            if (state.scene != SceneKey.Gone) {
-                                true to "scene is not Gone"
-                            } else {
-                                false to "scene is Gone"
+            deviceProvisioningInteractor.isFactoryResetProtectionActive
+                .flatMapLatest { isFrpActive ->
+                    if (isFrpActive) {
+                        flowOf(false to "Factory Reset Protection is active")
+                    } else {
+                        sceneInteractor.transitionState
+                            .mapNotNull { state ->
+                                when (state) {
+                                    is ObservableTransitionState.Idle -> {
+                                        if (state.scene != SceneKey.Gone) {
+                                            true to "scene is not Gone"
+                                        } else {
+                                            false to "scene is Gone"
+                                        }
+                                    }
+                                    is ObservableTransitionState.Transition -> {
+                                        if (state.fromScene == SceneKey.Gone) {
+                                            true to "scene transitioning away from Gone"
+                                        } else {
+                                            null
+                                        }
+                                    }
+                                }
                             }
-                        }
-                        is ObservableTransitionState.Transition -> {
-                            if (state.fromScene == SceneKey.Gone) {
-                                true to "scene transitioning away from Gone"
-                            } else {
-                                null
-                            }
-                        }
+                            .distinctUntilChanged()
                     }
                 }
-                .distinctUntilChanged()
                 .collect { (isVisible, loggingReason) ->
                     sceneInteractor.setVisible(isVisible, loggingReason)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 93cfc5d..2b978b2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -97,6 +97,10 @@
                     val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
                     view.addView(createVisibilityToggleView(legacyView))
 
+                    // This moves the SharedNotificationContainer to the WindowRootView just after
+                    //  the SceneContainerView. This SharedNotificationContainer should contain NSSL
+                    //  due to the NotificationStackScrollLayoutSection (legacy) or
+                    //  NotificationSection (scene container) moving it there.
                     if (flags.flexiNotifsEnabled()) {
                         (sharedNotificationContainer.parent as? ViewGroup)?.removeView(
                             sharedNotificationContainer
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 7aa0dad..b5a1313 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.media.MediaRecorder;
 import android.net.Uri;
@@ -379,10 +378,9 @@
                 .addExtras(extras);
 
         // Add thumbnail if available
-        Bitmap thumbnailBitmap = recording.getThumbnail();
-        if (thumbnailBitmap != null) {
+        if (recording.getThumbnail() != null) {
             Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
-                    .bigPicture(thumbnailBitmap)
+                    .bigPicture(recording.getThumbnail())
                     .showBigPictureWhenCollapsed(true);
             builder.setStyle(pictureStyle);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index a170d0da..5469a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -23,10 +23,12 @@
 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
 
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.media.MediaCodec;
@@ -51,6 +53,7 @@
 import android.view.Surface;
 import android.view.WindowManager;
 
+import com.android.internal.R;
 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget;
 
 import java.io.Closeable;
@@ -361,14 +364,27 @@
         Files.copy(mTempVideoFile.toPath(), os);
         os.close();
         if (mTempAudioFile != null) mTempAudioFile.delete();
-        DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
-        Size size = new Size(metrics.widthPixels, metrics.heightPixels);
-        SavedRecording recording = new SavedRecording(itemUri, mTempVideoFile, size);
+        SavedRecording recording = new SavedRecording(
+                itemUri, mTempVideoFile, getRequiredThumbnailSize());
         mTempVideoFile.delete();
         return recording;
     }
 
     /**
+     * Returns the required {@code Size} of the thumbnail.
+     */
+    private Size getRequiredThumbnailSize() {
+        boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+        int thumbnailIconHeight = mContext.getResources().getDimensionPixelSize(isLowRam
+                ? R.dimen.notification_big_picture_max_height_low_ram
+                : R.dimen.notification_big_picture_max_height);
+        int thumbnailIconWidth = mContext.getResources().getDimensionPixelSize(isLowRam
+                ? R.dimen.notification_big_picture_max_width_low_ram
+                : R.dimen.notification_big_picture_max_width);
+        return new Size(thumbnailIconWidth, thumbnailIconHeight);
+    }
+
+    /**
      * Release the resources without saving the data
      */
     protected void release() {
@@ -386,13 +402,14 @@
     public class SavedRecording {
 
         private Uri mUri;
-        private Bitmap mThumbnailBitmap;
+        private Icon mThumbnailIcon;
 
         protected SavedRecording(Uri uri, File file, Size thumbnailSize) {
             mUri = uri;
             try {
-                mThumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
+                Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
                         file, thumbnailSize, null);
+                mThumbnailIcon = Icon.createWithBitmap(thumbnailBitmap);
             } catch (IOException e) {
                 Log.e(TAG, "Error creating thumbnail", e);
             }
@@ -402,8 +419,8 @@
             return mUri;
         }
 
-        public @Nullable Bitmap getThumbnail() {
-            return mThumbnailBitmap;
+        public @Nullable Icon getThumbnail() {
+            return mThumbnailIcon;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 0053474..a01ac70 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -331,8 +331,8 @@
         );
         collectFlow(
                 mWindowRootView,
-                mCommunalInteractor.get().isCommunalShowing(),
-                this::onCommunalShowingChanged
+                mCommunalInteractor.get().isCommunalVisible(),
+                this::onCommunalVisibleChanged
         );
     }
 
@@ -475,6 +475,9 @@
             }
             visible = true;
             mLogger.d("Visibility forced to be true");
+        } else if (state.communalVisible) {
+            visible = true;
+            mLogger.d("Visibility forced to be true by communal");
         }
         if (mWindowRootView != null) {
             if (visible) {
@@ -510,15 +513,15 @@
     }
 
     private void applyUserActivityTimeout(NotificationShadeWindowState state) {
-        final Boolean communalShowing = state.isCommunalShowingAndNotOccluded();
+        final Boolean communalVisible = state.isCommunalVisibleAndNotOccluded();
         final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded();
         long timeout = -1;
-        if ((communalShowing || keyguardShowing)
+        if ((communalVisible || keyguardShowing)
                 && state.statusBarState == StatusBarState.KEYGUARD
                 && !state.qsExpanded) {
             if (state.bouncerShowing) {
                 timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS;
-            } else if (communalShowing) {
+            } else if (communalVisible) {
                 timeout = CommunalInteractor.AWAKE_INTERVAL_MS;
             } else if (keyguardShowing) {
                 timeout = mLockScreenDisplayTimeout;
@@ -624,7 +627,7 @@
                 state.dozing,
                 state.scrimsVisibility,
                 state.backgroundBlurRadius,
-                state.communalShowing
+                state.communalVisible
         );
     }
 
@@ -749,8 +752,8 @@
     }
 
     @VisibleForTesting
-    void onCommunalShowingChanged(Boolean showing) {
-        mCurrentState.communalShowing = showing;
+    void onCommunalVisibleChanged(Boolean visible) {
+        mCurrentState.communalVisible = visible;
         apply(mCurrentState);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index f9c9d83..e0a98b3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -58,15 +58,15 @@
     @JvmField var dreaming: Boolean = false,
     @JvmField var scrimsVisibility: Int = 0,
     @JvmField var backgroundBlurRadius: Int = 0,
-    @JvmField var communalShowing: Boolean = false,
+    @JvmField var communalVisible: Boolean = false,
 ) {
 
     fun isKeyguardShowingAndNotOccluded(): Boolean {
         return keyguardShowing && !keyguardOccluded
     }
 
-    fun isCommunalShowingAndNotOccluded(): Boolean {
-        return communalShowing && !keyguardOccluded
+    fun isCommunalVisibleAndNotOccluded(): Boolean {
+        return communalVisible && !keyguardOccluded
     }
 
     /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
@@ -99,7 +99,7 @@
             dozing.toString(),
             scrimsVisibility.toString(),
             backgroundBlurRadius.toString(),
-            communalShowing.toString(),
+            communalVisible.toString(),
         )
     }
 
@@ -140,7 +140,7 @@
             dozing: Boolean,
             scrimsVisibility: Int,
             backgroundBlurRadius: Int,
-            communalShowing: Boolean,
+            communalVisible: Boolean,
         ) {
             buffer.advance().apply {
                 this.keyguardShowing = keyguardShowing
@@ -172,7 +172,7 @@
                 this.dozing = dozing
                 this.scrimsVisibility = scrimsVisibility
                 this.backgroundBlurRadius = backgroundBlurRadius
-                this.communalShowing = communalShowing
+                this.communalVisible = communalVisible
             }
         }
 
@@ -218,7 +218,7 @@
                 "dozing",
                 "scrimsVisibility",
                 "backgroundBlurRadius",
-                "communalShowing"
+                "communalVisible"
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 19a5840..ef820f3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.app.StatusBarManager;
@@ -477,8 +478,9 @@
                         if (KeyguardShadeMigrationNssl.isEnabled()) {
                             // When on lockscreen, if the touch originates at the top of the screen
                             // go directly to QS and not the shade
-                            if (mQuickSettingsController.shouldQuickSettingsIntercept(
-                                    ev.getX(), ev.getY(), 0)) {
+                            if (mStatusBarStateController.getState() == KEYGUARD
+                                    && mQuickSettingsController.shouldQuickSettingsIntercept(
+                                        ev.getX(), ev.getY(), 0)) {
                                 mShadeLogger.d("NSWVC: QS intercepted");
                                 return true;
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index d3459b1..e40bcd5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -46,7 +46,7 @@
 
     /**
      * Adds a listener that will be notified when the panel expansion fraction has changed and
-     * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/23035507).
+     * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/281038056).
      *
      * @see #addExpansionListener
      */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index a71cf95..e619806 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -25,9 +25,10 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
+import com.android.systemui.util.kotlin.combine
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -44,7 +45,7 @@
 @Inject
 constructor(
     @Application val scope: CoroutineScope,
-    deviceProvisioningRepository: DeviceProvisioningRepository,
+    deviceProvisioningInteractor: DeviceProvisioningInteractor,
     disableFlagsRepository: DisableFlagsRepository,
     dozeParams: DozeParameters,
     keyguardRepository: KeyguardRepository,
@@ -56,7 +57,7 @@
 ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
     override val isShadeEnabled: StateFlow<Boolean> =
         combine(
-                deviceProvisioningRepository.isFactoryResetProtectionActive,
+                deviceProvisioningInteractor.isFactoryResetProtectionActive,
                 disableFlagsRepository.disableFlags,
             ) { isFrpActive, isDisabledByFlags ->
                 isDisabledByFlags.isShadeEnabled() && !isFrpActive
@@ -83,7 +84,7 @@
             powerInteractor.isAsleep,
             keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
             keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
-            deviceProvisioningRepository.isFactoryResetProtectionActive,
+            deviceProvisioningInteractor.isFactoryResetProtectionActive,
         ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
             when {
                 // Touches are disabled when Factory Reset Protection is active
@@ -103,7 +104,7 @@
             isShadeEnabled,
             keyguardRepository.isDozing,
             userSetupRepository.isUserSetUp,
-            deviceProvisioningRepository.isDeviceProvisioned,
+            deviceProvisioningInteractor.isDeviceProvisioned,
         ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned ->
             isDeviceProvisioned &&
                 // Disallow QS during setup if it's a simple user switcher. (The user intends to
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index ada7d3e..ffb11dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -56,6 +56,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
@@ -305,6 +306,13 @@
         default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { }
 
         default void addQsTile(ComponentName tile) { }
+
+        /**
+         * Add a tile to the Quick Settings Panel
+         * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+         * @param end if true, the tile will be added at the end. If false, at the beginning.
+         */
+        default void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { }
         default void remQsTile(ComponentName tile) { }
 
         default void setQsTiles(String[] tiles) {}
@@ -904,8 +912,29 @@
 
     @Override
     public void addQsTile(ComponentName tile) {
-        synchronized (mLock) {
-            mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+        if (Flags.a11yQsShortcut()) {
+            addQsTileToFrontOrEnd(tile, false);
+        } else {
+            synchronized (mLock) {
+                mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+            }
+        }
+    }
+
+    /**
+     * Add a tile to the Quick Settings Panel
+     * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+     * @param end if true, the tile will be added at the end. If false, at the beginning.
+     */
+    @Override
+    public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+        if (Flags.a11yQsShortcut()) {
+            synchronized (mLock) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = tile;
+                args.arg2 = end;
+                mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget();
+            }
         }
     }
 
@@ -1546,11 +1575,21 @@
                         mCallbacks.get(i).showPictureInPictureMenu();
                     }
                     break;
-                case MSG_ADD_QS_TILE:
-                    for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+                case MSG_ADD_QS_TILE: {
+                    if (Flags.a11yQsShortcut()) {
+                        SomeArgs someArgs = (SomeArgs) msg.obj;
+                        for (int i = 0; i < mCallbacks.size(); i++) {
+                            mCallbacks.get(i).addQsTileToFrontOrEnd(
+                                    (ComponentName) someArgs.arg1, (boolean) someArgs.arg2);
+                        }
+                        someArgs.recycle();
+                    } else {
+                        for (int i = 0; i < mCallbacks.size(); i++) {
+                            mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+                        }
                     }
                     break;
+                }
                 case MSG_REMOVE_QS_TILE:
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).remQsTile((ComponentName) msg.obj);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index d6d3e67..04d9b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -591,8 +591,10 @@
     }
 
     private void updateLockScreenUserLockedMsg(int userId) {
-        if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)
-                || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) {
+        boolean userUnlocked = mKeyguardUpdateMonitor.isUserUnlocked(userId);
+        boolean encryptedOrLockdown = mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId);
+        mKeyguardLogger.logUpdateLockScreenUserLockedMsg(userId, userUnlocked, encryptedOrLockdown);
+        if (!userUnlocked || encryptedOrLockdown) {
             mRotateTextViewController.updateIndication(
                     INDICATION_TYPE_USER_LOCKED,
                     new KeyguardIndication.Builder()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 28d4457..fc84973 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -35,6 +35,7 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
@@ -60,6 +61,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.Dumpable;
+import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -71,7 +73,6 @@
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -84,8 +85,6 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 
-import kotlin.Unit;
-
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -100,6 +99,8 @@
 
 import javax.inject.Inject;
 
+import kotlin.Unit;
+
 /** Platform implementation of the network controller. **/
 @SysUISingleton
 public class NetworkControllerImpl extends BroadcastReceiver
@@ -349,7 +350,7 @@
         // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
         updateAirplaneMode(true /* force callback */);
         mUserTracker = userTracker;
-        mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
 
         deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 342828c..aca8b64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -6,7 +6,6 @@
 import android.net.Uri
 import android.os.Handler
 import android.os.HandlerExecutor
-import android.os.HandlerThread
 import android.os.UserHandle
 import android.provider.Settings
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -88,7 +87,6 @@
             secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
     private val onStateChangedListeners = ListenerSet<Consumer<String>>()
     private var hideSilentNotificationsOnLockscreen: Boolean = false
-    private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis")
 
     private val userTrackerCallback = object : UserTracker.Callback {
         override fun onUserChanged(newUser: Int, userContext: Context) {
@@ -156,9 +154,7 @@
                 notifyStateChanged("onStatusBarUpcomingStateChanged")
             }
         })
-        handlerThread.start()
-        userTracker.addCallback(userTrackerCallback,
-                   HandlerExecutor(handlerThread.getThreadHandler()))
+        userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler))
     }
 
     override fun addOnStateChangedListener(listener: Consumer<String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index dd04531..b9afb14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -617,7 +617,11 @@
     private final ScrollAdapter mScrollAdapter = new ScrollAdapter() {
         @Override
         public boolean isScrolledToTop() {
-            return mOwnScrollY == 0;
+            if (SceneContainerFlag.isEnabled()) {
+                return mController.isPlaceholderScrolledToTop();
+            } else {
+                return mOwnScrollY == 0;
+            }
         }
 
         @Override
@@ -1442,7 +1446,14 @@
             fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
         }
         final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
-        mAmbientState.setStackY(stackY);
+        // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
+        if (SceneContainerFlag.isEnabled()) {
+            // stackY should be driven by scene container, not NSSL
+            mAmbientState.setStackY(mTopPadding);
+        } else {
+            mAmbientState.setStackY(stackY);
+        }
+
         if (mOnStackYChanged != null) {
             mOnStackYChanged.accept(listenerNeedsAnimation);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 49fde39..ed26677 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1144,6 +1144,14 @@
         return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
     }
 
+    /**
+     * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled
+     * down any further.
+     */
+    public boolean isPlaceholderScrolledToTop() {
+        return mStackAppearanceInteractor.getScrolledToTop().getValue();
+    }
+
     /** Set the intrinsic height of the stack content without additional padding. */
     public void setIntrinsicContentHeight(float intrinsicContentHeight) {
         mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index aac3c28..0197264 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -44,4 +44,10 @@
      * screen.
      */
     val contentTop = MutableStateFlow(0f)
+
+    /**
+     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+     * further.
+     */
+    val scrolledToTop = MutableStateFlow(true)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 1dfde09..8307397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -42,10 +42,16 @@
      * notifications, this can exceed the space available on screen to show notifications, at which
      * point the notification stack should become scrollable.
      */
-    val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow()
+    val intrinsicContentHeight: StateFlow<Float> = repository.intrinsicContentHeight.asStateFlow()
 
     /** The y-coordinate in px of top of the contents of the notification stack. */
-    val contentTop = repository.contentTop.asStateFlow()
+    val contentTop: StateFlow<Float> = repository.contentTop.asStateFlow()
+
+    /**
+     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+     * further.
+     */
+    val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
 
     /** Sets the position of the notification stack in the current scene. */
     fun setStackBounds(bounds: NotificationContainerBounds) {
@@ -62,4 +68,9 @@
     fun setContentTop(startY: Float) {
         repository.contentTop.value = startY
     }
+
+    /** Sets whether the notification stack is scrolled to the top. */
+    fun setScrolledToTop(scrolledToTop: Boolean) {
+        repository.scrolledToTop.value = scrolledToTop
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index ed15f55..6c2cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.pow
 import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
@@ -65,7 +64,9 @@
                     viewModel.expandFraction.collect { expandFraction ->
                         ambientState.expansionFraction = expandFraction
                         controller.expandedHeight = expandFraction * controller.view.height
-                        controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f))
+                        controller.setMaxAlphaForExpansion(
+                            ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index 74db583..56ff7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -19,11 +19,15 @@
 
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
 @SysUISingleton
@@ -32,9 +36,40 @@
 constructor(
     stackAppearanceInteractor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
+    sceneInteractor: SceneInteractor,
 ) {
-    /** The expansion fraction from the top of the notification shade. */
-    val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+    /**
+     * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
+     * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
+     * transitioning from Shade to QuickSettings scenes.
+     */
+    val expandFraction: Flow<Float> =
+        combine(
+            shadeInteractor.shadeExpansion,
+            sceneInteractor.transitionState,
+        ) { shadeExpansion, transitionState ->
+            when (transitionState) {
+                is ObservableTransitionState.Idle -> {
+                    if (transitionState.scene == SceneKey.Lockscreen) {
+                        1f
+                    } else {
+                        shadeExpansion
+                    }
+                }
+                is ObservableTransitionState.Transition -> {
+                    if (
+                        (transitionState.fromScene == SceneKey.Shade &&
+                            transitionState.toScene == SceneKey.QuickSettings) ||
+                            (transitionState.fromScene == SceneKey.QuickSettings &&
+                                transitionState.toScene == SceneKey.Shade)
+                    ) {
+                        1f
+                    } else {
+                        shadeExpansion
+                    }
+                }
+            }
+        }
 
     /** The bounds of the notification stack in the current scene. */
     val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 385f061..a436f17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -87,4 +87,9 @@
     fun onContentTopChanged(padding: Float) {
         interactor.setContentTop(padding)
     }
+
+    /** Sets whether the notification stack is scrolled to the top. */
+    fun setScrolledToTop(scrolledToTop: Boolean) {
+        interactor.setScrolledToTop(scrolledToTop)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 4617ce4..3915c376 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -24,13 +24,24 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
@@ -51,6 +62,7 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -69,43 +81,52 @@
     communalInteractor: CommunalInteractor,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
+    dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
     glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
     lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
     private val aodBurnInViewModel: AodBurnInViewModel,
 ) {
-    private val statesForConstrainedNotifications =
-        setOf(
-            KeyguardState.AOD,
-            KeyguardState.LOCKSCREEN,
-            KeyguardState.DOZING,
-            KeyguardState.ALTERNATE_BOUNCER,
-            KeyguardState.PRIMARY_BOUNCER
+    private val statesForConstrainedNotifications: Set<KeyguardState> =
+        setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
+
+    private val edgeToAlphaViewModel =
+        mapOf<Edge?, Flow<Float>>(
+            Edge(from = LOCKSCREEN, to = DREAMING) to
+                lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+            Edge(from = DREAMING, to = LOCKSCREEN) to
+                dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+            Edge(from = LOCKSCREEN, to = OCCLUDED) to
+                lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+            Edge(from = OCCLUDED, to = LOCKSCREEN) to
+                occludedToLockscreenTransitionViewModel.lockscreenAlpha,
         )
 
-    private val lockscreenToOccludedRunning =
-        keyguardTransitionInteractor
-            .transition(KeyguardState.LOCKSCREEN, KeyguardState.OCCLUDED)
-            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+    private val lockscreenTransitionInProgress: Flow<Edge?> =
+        keyguardTransitionInteractor.transitions
+            .map { step ->
+                if (
+                    (step.transitionState == STARTED || step.transitionState == RUNNING) &&
+                        (step.from == LOCKSCREEN || step.to == LOCKSCREEN)
+                ) {
+                    Edge(step.from, step.to)
+                } else {
+                    null
+                }
+            }
             .distinctUntilChanged()
-            .onStart { emit(false) }
-
-    private val occludedToLockscreenRunning =
-        keyguardTransitionInteractor
-            .transition(KeyguardState.OCCLUDED, KeyguardState.LOCKSCREEN)
-            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
-            .distinctUntilChanged()
-            .onStart { emit(false) }
+            .onStart { emit(null) }
 
     private val lockscreenToGlanceableHubRunning =
         keyguardTransitionInteractor
-            .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            .transition(LOCKSCREEN, GLANCEABLE_HUB)
             .map { it.transitionState == STARTED || it.transitionState == RUNNING }
             .distinctUntilChanged()
             .onStart { emit(false) }
 
     private val glanceableHubToLockscreenRunning =
         keyguardTransitionInteractor
-            .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+            .transition(GLANCEABLE_HUB, LOCKSCREEN)
             .map { it.transitionState == STARTED || it.transitionState == RUNNING }
             .distinctUntilChanged()
             .onStart { emit(false) }
@@ -141,7 +162,7 @@
                     statesForConstrainedNotifications.contains(it)
                 },
                 keyguardTransitionInteractor
-                    .transitionValue(KeyguardState.LOCKSCREEN)
+                    .transitionValue(LOCKSCREEN)
                     .onStart { emit(0f) }
                     .map { it > 0 }
             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
@@ -242,38 +263,46 @@
                 initialValue = NotificationContainerBounds(),
             )
 
+    /** As QS is expanding, fade out notifications unless in splitshade */
+    private val alphaForQsExpansion: Flow<Float> =
+        interactor.configurationBasedDimensions.flatMapLatest {
+            if (it.useSplitShade) {
+                flowOf(1f)
+            } else {
+                shadeInteractor.qsExpansion.map { 1f - it }
+            }
+        }
+
     val expansionAlpha: Flow<Float> =
         // Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
-        // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition
+        // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
         // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
         // those transitions are in progress. Without this, the alpha value will produce a visible
         // flicker.
-        lockscreenToOccludedRunning.flatMapLatest { isLockscreenToOccludedRunning ->
-            if (isLockscreenToOccludedRunning) {
-                lockscreenToOccludedTransitionViewModel.lockscreenAlpha
-            } else {
-                occludedToLockscreenRunning.flatMapLatest { isOccludedToLockscreenRunning ->
-                    if (isOccludedToLockscreenRunning) {
-                        occludedToLockscreenTransitionViewModel.lockscreenAlpha.onStart { emit(0f) }
-                    } else {
+        lockscreenTransitionInProgress
+            .flatMapLatest { edge ->
+                edgeToAlphaViewModel.getOrElse(
+                    edge,
+                    {
                         isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
                             combineTransform(
                                 keyguardInteractor.keyguardAlpha,
                                 shadeCollpaseFadeIn,
-                            ) { alpha, shadeCollpaseFadeIn ->
+                                alphaForQsExpansion,
+                            ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
                                 if (isOnLockscreenWithoutShade) {
                                     if (!shadeCollpaseFadeIn) {
                                         emit(alpha)
                                     }
                                 } else {
-                                    emit(1f)
+                                    emit(alphaForQsExpansion)
                                 }
                             }
                         }
                     }
-                }
+                )
             }
-        }
+            .distinctUntilChanged()
 
     /**
      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 60a4606..39ca7b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -179,6 +179,11 @@
     }
 
     @Override
+    public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+        mQSHost.addTile(tile, end);
+    }
+
+    @Override
     public void remQsTile(ComponentName tile) {
         mQSHost.removeTileByUser(tile);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index ade417d..64fcef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -125,6 +125,7 @@
 import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -245,6 +246,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -551,6 +553,25 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     protected final PowerInteractor mPowerInteractor;
 
+    private final CommunalInteractor mCommunalInteractor;
+
+    /**
+     * True if the device is showing the glanceable hub. See
+     * {@link CommunalInteractor#isIdleOnCommunal()} for more details.
+     */
+    private boolean mIsIdleOnCommunal = false;
+    private final Consumer<Boolean> mIdleOnCommunalConsumer = (Boolean idleOnCommunal) -> {
+        if (idleOnCommunal == mIsIdleOnCommunal) {
+            // Ignore initial value coming through the flow.
+            return;
+        }
+
+        mIsIdleOnCommunal = idleOnCommunal;
+        // Trigger an update for the scrim state when we enter or exit glanceable hub, so that we
+        // can transition to/from ScrimState.GLANCEABLE_HUB if needed.
+        updateScrimController();
+    };
+
     private boolean mNoAnimationOnNextBarModeChange;
     private final SysuiStatusBarStateController mStatusBarStateController;
 
@@ -618,6 +639,7 @@
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
             PowerInteractor powerInteractor,
+            CommunalInteractor communalInteractor,
             SysuiStatusBarStateController statusBarStateController,
             Optional<Bubbles> bubblesOptional,
             Lazy<NoteTaskController> noteTaskControllerLazy,
@@ -722,6 +744,7 @@
         mScreenLifecycle = screenLifecycle;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mPowerInteractor = powerInteractor;
+        mCommunalInteractor = communalInteractor;
         mStatusBarStateController = statusBarStateController;
         mBubblesOptional = bubblesOptional;
         mNoteTaskControllerLazy = noteTaskControllerLazy;
@@ -1051,6 +1074,10 @@
         //TODO(b/264502026) move the rest of the listeners here.
         mDeviceStateManager.registerCallback(mMainExecutor,
                 new FoldStateListener(mContext, this::onFoldedStateChanged));
+
+        mJavaAdapter.alwaysCollectFlow(
+                mCommunalInteractor.isIdleOnCommunal(),
+                mIdleOnCommunalConsumer);
     }
 
     /**
@@ -2795,6 +2822,8 @@
             // This will cancel the keyguardFadingAway animation if it is running. We need to do
             // this as otherwise it can remain pending and leave keyguard in a weird state.
             mUnlockScrimCallback.onCancelled();
+        } else if (mIsIdleOnCommunal) {
+            mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
         } else if (mKeyguardStateController.isShowing()
                 && !mKeyguardStateController.isOccluded()
                 && !unlocking) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
deleted file mode 100644
index 9f08633..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.phone
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
-import com.android.keyguard.ActiveUnlockConfig
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.CoreStartable
-import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.Assert
-import com.android.systemui.util.sensors.AsyncSensorManager
-import java.io.PrintWriter
-import javax.inject.Inject
-
-/**
- * Triggers face auth on lift when the device is showing the lock screen. Only initialized
- * if face auth is supported on the device. Not to be confused with the lift to wake gesture
- * which is handled by {@link com.android.server.policy.PhoneWindowManager}.
- */
-@SysUISingleton
-class KeyguardLiftController @Inject constructor(
-        private val context: Context,
-        private val statusBarStateController: StatusBarStateController,
-        private val asyncSensorManager: AsyncSensorManager,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-        private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
-        private val dumpManager: DumpManager,
-        private val selectedUserInteractor: SelectedUserInteractor,
-) : Dumpable, CoreStartable {
-
-    private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
-    private var isListening = false
-    private var bouncerVisible = false
-
-    override fun start() {
-        if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
-            init()
-        }
-    }
-
-    private fun init() {
-        dumpManager.registerDumpable(this)
-        statusBarStateController.addCallback(statusBarStateListener)
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        updateListeningState()
-    }
-
-    private val listener: TriggerEventListener = object : TriggerEventListener() {
-        override fun onTrigger(event: TriggerEvent?) {
-            Assert.isMainThread()
-            // Not listening anymore since trigger events unregister themselves
-            isListening = false
-            updateListeningState()
-            deviceEntryFaceAuthInteractor.onDeviceLifted()
-            keyguardUpdateMonitor.requestActiveUnlock(
-                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
-                "KeyguardLiftController")
-        }
-    }
-
-    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
-        override fun onKeyguardBouncerFullyShowingChanged(bouncer: Boolean) {
-            bouncerVisible = bouncer
-            updateListeningState()
-        }
-
-        override fun onKeyguardVisibilityChanged(visible: Boolean) {
-            updateListeningState()
-        }
-    }
-
-    private val statusBarStateListener = object : StatusBarStateController.StateListener {
-        override fun onDozingChanged(isDozing: Boolean) {
-            updateListeningState()
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.println("KeyguardLiftController:")
-        pw.println("  pickupSensor: $pickupSensor")
-        pw.println("  isListening: $isListening")
-        pw.println("  bouncerVisible: $bouncerVisible")
-    }
-
-    private fun updateListeningState() {
-        if (pickupSensor == null) {
-            return
-        }
-        val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible &&
-                !statusBarStateController.isDozing
-
-        val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
-        val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled
-        if (shouldListen != isListening) {
-            isListening = shouldListen
-
-            if (shouldListen) {
-                asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
-            } else {
-                asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 3f20eaf..6f78604 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.phone;
 
 import static com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
@@ -62,6 +64,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.ScrimAlpha;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -292,6 +295,30 @@
                 mScrimBehind.setViewAlpha(mBehindAlpha);
             };
 
+    /**
+     * Consumer that fades the behind scrim in and out during the transition between the lock screen
+     * and the glanceable hub.
+     *
+     * While the lock screen is showing, the behind scrim is used to slightly darken the lock screen
+     * wallpaper underneath. Since the glanceable hub is under all of the scrims, we want to fade
+     * out the scrim so that the glanceable hub isn't darkened when it opens.
+     *
+     * {@link #applyState()} handles the scrim alphas once on the glanceable hub, this is only
+     * responsible for setting the behind alpha during the transition.
+     */
+    private final Consumer<TransitionStep> mGlanceableHubConsumer = (TransitionStep step) -> {
+        final float baseAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+        final float transitionProgress = step.getValue();
+        if (step.getTo() == KeyguardState.LOCKSCREEN) {
+            // Transitioning back to lock screen, fade in behind scrim again.
+            mBehindAlpha = baseAlpha * transitionProgress;
+        } else if (step.getTo() == GLANCEABLE_HUB) {
+            // Transitioning to glanceable hub, fade out behind scrim.
+            mBehindAlpha = baseAlpha * (1 - transitionProgress);
+        }
+        mScrimBehind.setViewAlpha(mBehindAlpha);
+    };
+
     Consumer<TransitionStep> mBouncerToGoneTransition;
 
     @Inject
@@ -444,6 +471,14 @@
                 mBouncerToGoneTransition, mMainDispatcher);
         collectFlow(behindScrim, mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha(),
                 mScrimAlphaConsumer, mMainDispatcher);
+
+        // LOCKSCREEN<->GLANCEABLE_HUB
+        collectFlow(behindScrim,
+                mKeyguardTransitionInteractor.transition(LOCKSCREEN, GLANCEABLE_HUB),
+                mGlanceableHubConsumer, mMainDispatcher);
+        collectFlow(behindScrim,
+                mKeyguardTransitionInteractor.transition(GLANCEABLE_HUB, LOCKSCREEN),
+                mGlanceableHubConsumer, mMainDispatcher);
     }
 
     // TODO(b/270984686) recompute scrim height accurately, based on shade contents.
@@ -815,9 +850,9 @@
             return;
         }
         mBouncerHiddenFraction = bouncerHiddenAmount;
-        if (mState == ScrimState.DREAMING) {
-            // Only the dreaming state requires this for the scrim calculation, so we should
-            // only trigger an update if dreaming.
+        if (mState == ScrimState.DREAMING || mState == ScrimState.GLANCEABLE_HUB) {
+            // The dreaming and glanceable hub states requires this for the scrim calculation, so we
+            // should only trigger an update in those states.
             applyAndDispatchState();
         }
     }
@@ -939,7 +974,7 @@
         } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
             mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f);
         } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
-                || mState == ScrimState.PULSING) {
+                || mState == ScrimState.PULSING || mState == ScrimState.GLANCEABLE_HUB) {
             Pair<Integer, Float> result = calculateBackStateForState(mState);
             int behindTint = result.first;
             float behindAlpha = result.second;
@@ -950,6 +985,11 @@
                         mTransitionToFullShadeProgress);
                 behindTint = ColorUtils.blendARGB(behindTint, shadeResult.first,
                         mTransitionToFullShadeProgress);
+            } else if (mState == ScrimState.GLANCEABLE_HUB && mTransitionToFullShadeProgress == 0.0f
+                    && mBouncerHiddenFraction == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
+                // Behind scrim should not be visible when idle on the glanceable hub and neither
+                // bouncer nor shade are showing.
+                behindAlpha = 0f;
             }
             mInFrontAlpha = mState.getFrontAlpha();
             if (mClipsQsScrim) {
@@ -965,6 +1005,13 @@
                 } else if (mState == ScrimState.SHADE_LOCKED) {
                     // going from KEYGUARD to SHADE_LOCKED state
                     mNotificationsAlpha = getInterpolatedFraction();
+                } else if (mState == ScrimState.GLANCEABLE_HUB
+                        && mTransitionToFullShadeProgress == 0.0f) {
+                    // Notification scrim should not be visible on the glanceable hub unless the
+                    // shade is showing or transitioning in. Otherwise the notification scrim will
+                    // be visible as the bouncer transitions in or after the notification shade
+                    // closes.
+                    mNotificationsAlpha = 0;
                 } else {
                     mNotificationsAlpha = Math.max(1.0f - getInterpolatedFraction(), mQsExpansion);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 61bd112..f2a649b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -296,6 +296,21 @@
                 updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor);
             }
         }
+    },
+
+    /**
+     * Device is locked or on dream and user has swiped from the right edge to enter the glanceable
+     * hub UI. From this state, the user can swipe from the left edge to go back to the lock screen
+     * or dream, as well as swipe down for the notifications and up for the bouncer.
+     */
+    GLANCEABLE_HUB {
+        @Override
+        public void prepare(ScrimState previousState) {
+            // No scrims should be visible by default in this state.
+            mBehindAlpha = 0;
+            mNotifAlpha = 0;
+            mFrontAlpha = 0;
+        }
     };
 
     boolean mBlankScreen = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 0051161..1c33d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
 import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
 import javax.inject.Inject
@@ -40,6 +41,7 @@
 constructor(
     interactor: DeviceBasedSatelliteInteractor,
     @Application scope: CoroutineScope,
+    airplaneModeRepository: AirplaneModeRepository,
 ) {
     private val shouldShowIcon: StateFlow<Boolean> =
         interactor.areAllConnectionsOutOfService
@@ -47,7 +49,11 @@
                 if (!allOos) {
                     flowOf(false)
                 } else {
-                    interactor.isSatelliteAllowed
+                    combine(interactor.isSatelliteAllowed, airplaneModeRepository.isAirplaneMode) {
+                        isSatelliteAllowed,
+                        isAirplaneMode ->
+                        isSatelliteAllowed && !isAirplaneMode
+                    }
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index a2d8d15..20d1fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -28,8 +28,6 @@
 import android.icu.text.DateTimePatternGenerator;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -50,11 +48,11 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
 import com.android.systemui.FontSizeUtils;
+import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -108,7 +106,6 @@
     private final int mAmPmStyle;
     private boolean mShowSeconds;
     private Handler mSecondsHandler;
-    private HandlerThread mHandlerThread;
 
     // Fields to cache the width so the clock remains at an approximately constant width
     private int mCharsAtCurrentWidth = -1;
@@ -149,8 +146,6 @@
         }
         mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
         mUserTracker = Dependency.get(UserTracker.class);
-        mHandlerThread = new HandlerThread("Clock");
-        mHandlerThread.start();
 
         setIncludeFontPadding(false);
     }
@@ -210,8 +205,7 @@
             Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
                     StatusBarIconController.ICON_HIDE_LIST);
             mCommandQueue.addCallback(this);
-            mUserTracker.addCallback(mUserChangedCallback,
-                       new HandlerExecutor(mHandlerThread.getThreadHandler()));
+            mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
             mCurrentUserId = mUserTracker.getUserId();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
index a7440d6..b7d8ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -21,8 +21,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
@@ -53,7 +51,6 @@
     private final UserTracker mUserTracker;
     private AlarmManager mAlarmManager;
     private AlarmManager.AlarmClockInfo mNextAlarm;
-    private HandlerThread mHandlerThread;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -78,10 +75,7 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
         broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL);
-        mHandlerThread = new HandlerThread("NextAlarmControllerImpl");
-        mHandlerThread.start();
-        mUserTracker.addCallback(mUserChangedCallback,
-                    new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
         updateNextAlarm();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 6a6efbc..9f4a906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -157,7 +157,7 @@
         // TODO: re-register network callback on user change.
         mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
         onUserSwitched(mUserTracker.getUserId());
-        mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
index 0bc0e88..2ed9d15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -36,9 +36,9 @@
 
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.drawable.UserIconDrawable;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.res.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.settings.UserTracker;
 
 import java.util.ArrayList;
@@ -66,11 +66,11 @@
     /**
      */
     @Inject
-    public UserInfoControllerImpl(Context context, @Background Executor bgExecutor,
+    public UserInfoControllerImpl(Context context, @Main Executor mainExecutor,
             UserTracker userTracker) {
         mContext = context;
         mUserTracker = userTracker;
-        mUserTracker.addCallback(mUserChangedCallback, bgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
 
         IntentFilter profileFilter = new IntentFilter();
         profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index f0b4930..df210b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -29,7 +29,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -82,7 +81,6 @@
     private volatile int mZenMode;
     private long mZenUpdateTime;
     private NotificationManager.Policy mConsolidatedNotificationPolicy;
-    private HandlerThread mHandlerThread;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -135,8 +133,6 @@
                 }
             }
         };
-        mHandlerThread = new HandlerThread("ZenModeControllerImpl");
-        mHandlerThread.start();
         mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver);
         updateZenMode(getModeSettingValueFromProvider());
@@ -147,8 +143,7 @@
         mSetupObserver = new SetupObserver(handler);
         mSetupObserver.register();
         mUserManager = context.getSystemService(UserManager.class);
-        mUserTracker.addCallback(mUserChangedCallback,
-                new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler));
         // This registers the alarm broadcast receiver for the current user
         mUserChangedCallback.onUserChanged(getCurrentUser(), context);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
new file mode 100644
index 0000000..32cf86d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates device-provisioning related business logic. */
+@SysUISingleton
+class DeviceProvisioningInteractor
+@Inject
+constructor(
+    repository: DeviceProvisioningRepository,
+) {
+    /**
+     * Whether this device has been provisioned.
+     *
+     * @see android.provider.Settings.Global.DEVICE_PROVISIONED
+     */
+    val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
+
+    /** Whether Factory Reset Protection (FRP) is currently active, locking the device. */
+    val isFactoryResetProtectionActive: Flow<Boolean> = repository.isFactoryResetProtectionActive
+}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 77518db..2b9ad50 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -480,7 +480,7 @@
             return;
         }
 
-        mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
 
         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
 
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index f5b4d17..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -26,7 +26,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -39,11 +38,11 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.systemui.DejankUtils;
+import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.qs.QSHost;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -99,7 +98,6 @@
     private UserTracker.Callback mCurrentUserTracker;
     private UserTracker mUserTracker;
     private final ComponentName mTunerComponent;
-    private HandlerThread mHandlerThread;
 
     /**
      */
@@ -119,8 +117,7 @@
         mDemoModeController = demoModeController;
         mUserTracker = userTracker;
         mTunerComponent = new ComponentName(mContext, TunerActivity.class);
-        mHandlerThread = new HandlerThread("TunerServiceImpl");
-        mHandlerThread.start();
+
         for (UserInfo user : UserManager.get(mContext).getUsers()) {
             mCurrentUser = user.getUserHandle().getIdentifier();
             if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
@@ -138,7 +135,7 @@
             }
         };
         mUserTracker.addCallback(mCurrentUserTracker,
-                new HandlerExecutor(mHandlerThread.getThreadHandler()));
+                new HandlerExecutor(mainHandler));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 74e1339..37be1c6 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.user.data.repository
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.pm.UserInfo
 import android.os.UserHandle
@@ -190,7 +191,7 @@
                         }
                     }
 
-                tracker.addCallback(callback, backgroundDispatcher.asExecutor())
+                tracker.addCallback(callback, mainDispatcher.asExecutor())
                 send(currentSelectionStatus)
 
                 awaitClose { tracker.removeCallback(callback) }
@@ -209,18 +210,15 @@
 
     override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo }
 
+    @SuppressLint("MissingPermission")
     override fun refreshUsers() {
         applicationScope.launch {
-            val result = withContext(backgroundDispatcher) { manager.aliveUsers }
-
-            if (result != null) {
-                _userInfos.value =
-                    result
-                        // Users should be sorted by ascending creation time.
-                        .sortedBy { it.creationTime }
-                        // The guest user is always last, regardless of creation time.
-                        .sortedBy { it.isGuest }
-            }
+            _userInfos.value =
+                withContext(backgroundDispatcher) { manager.aliveUsers }
+                    // Users should be sorted by ascending creation time.
+                    .sortedBy { it.creationTime }
+                    // The guest user is always last, regardless of creation time.
+                    .sortedBy { it.isGuest }
 
             if (mainUserId == UserHandle.USER_NULL) {
                 val mainUser = withContext(backgroundDispatcher) { manager.mainUser }
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
index adae782..31a8d86 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -26,7 +26,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.flowOn
 
 /** Utility class that could give information about if animation are enabled in the system */
 interface AnimationStatusRepository {
@@ -45,24 +45,26 @@
      * Emits true if animations are enabled in the system, after subscribing it immediately emits
      * the current state
      */
-    override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow {
-        val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() }
-        trySend(initialValue)
+    override fun areAnimationsEnabled(): Flow<Boolean> =
+        conflatedCallbackFlow {
+                val initialValue = resolver.areAnimationsEnabled()
+                trySend(initialValue)
 
-        val observer =
-            object : ContentObserver(backgroundHandler) {
-                override fun onChange(selfChange: Boolean) {
-                    val updatedValue = resolver.areAnimationsEnabled()
-                    trySend(updatedValue)
-                }
+                val observer =
+                    object : ContentObserver(backgroundHandler) {
+                        override fun onChange(selfChange: Boolean) {
+                            val updatedValue = resolver.areAnimationsEnabled()
+                            trySend(updatedValue)
+                        }
+                    }
+
+                resolver.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+                    /* notifyForDescendants= */ false,
+                    observer
+                )
+
+                awaitClose { resolver.unregisterContentObserver(observer) }
             }
-
-        resolver.registerContentObserver(
-            Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
-            /* notifyForDescendants= */ false,
-            observer
-        )
-
-        awaitClose { resolver.unregisterContentObserver(observer) }
-    }
+            .flowOn(backgroundDispatcher)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 8fe57e11..d47413f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -20,21 +20,19 @@
 import com.android.systemui.util.time.SystemClockImpl
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.math.max
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /**
@@ -106,6 +104,14 @@
 /** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
 data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
 
+/** Emits a [Unit] only when the number of downstream subscribers of this flow increases. */
+fun <T> MutableSharedFlow<T>.onSubscriberAdded(): Flow<Unit> {
+    return subscriptionCount
+        .pairwise(initialValue = 0)
+        .filter { (previous, current) -> current > previous }
+        .map {}
+}
+
 /**
  * Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
  * [transform].
@@ -183,34 +189,6 @@
 
 /**
  * Returns a flow that mirrors the original flow, but delays values following emitted values for the
- * given [periodMs]. If the original flow emits more than one value during this period, only the
- * latest value is emitted.
- *
- * Example:
- * ```kotlin
- * flow {
- *     emit(1)     // t=0ms
- *     delay(90)
- *     emit(2)     // t=90ms
- *     delay(90)
- *     emit(3)     // t=180ms
- *     delay(1010)
- *     emit(4)     // t=1190ms
- *     delay(1010)
- *     emit(5)     // t=2200ms
- * }.throttle(1000)
- * ```
- *
- * produces the following emissions at the following times
- *
- * ```text
- * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
- * ```
- */
-fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl())
-
-/**
- * Returns a flow that mirrors the original flow, but delays values following emitted values for the
  * given [periodMs] as reported by the given [clock]. If the original flow emits more than one value
  * during this period, only The latest value is emitted.
  *
@@ -235,70 +213,37 @@
  * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
  * ```
  */
-fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow {
-    coroutineScope {
-        var previousEmitTimeMs = 0L
-        var delayJob: Job? = null
-        var sendJob: Job? = null
-        val outerScope = this
+fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl()): Flow<T> =
+    channelFlow {
+        coroutineScope {
+            var previousEmitTimeMs = 0L
+            var delayJob: Job? = null
+            var sendJob: Job? = null
+            val outerScope = this
 
-        collect {
-            delayJob?.cancel()
-            sendJob?.join()
-            val currentTimeMs = clock.elapsedRealtime()
-            val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
-            val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
-            if (timeUntilNextEmit > 0L) {
-                // We create delayJob to allow cancellation during the delay period
-                delayJob = launch {
-                    delay(timeUntilNextEmit)
-                    sendJob =
-                        outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                            send(it)
-                            previousEmitTimeMs = clock.elapsedRealtime()
-                        }
+            collect {
+                delayJob?.cancel()
+                sendJob?.join()
+                val currentTimeMs = clock.elapsedRealtime()
+                val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+                val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
+                if (timeUntilNextEmit > 0L) {
+                    // We create delayJob to allow cancellation during the delay period
+                    delayJob = launch {
+                        delay(timeUntilNextEmit)
+                        sendJob =
+                            outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+                                send(it)
+                                previousEmitTimeMs = clock.elapsedRealtime()
+                            }
+                    }
+                } else {
+                    send(it)
+                    previousEmitTimeMs = currentTimeMs
                 }
-            } else {
-                send(it)
-                previousEmitTimeMs = currentTimeMs
             }
         }
     }
-}
-
-/**
- * Returns a [StateFlow] launched in the surrounding [CoroutineScope]. This [StateFlow] gets its
- * value by invoking [getValue] whenever an event is emitted from [changedSignals]. It will also
- * immediately invoke [getValue] to establish its initial value.
- */
-inline fun <T> CoroutineScope.stateFlow(
-    changedSignals: Flow<*>,
-    crossinline getValue: () -> T,
-): StateFlow<T> =
-    changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
-
-inline fun <T1, T2, T3, T4, T5, T6, R> combine(
-    flow: Flow<T1>,
-    flow2: Flow<T2>,
-    flow3: Flow<T3>,
-    flow4: Flow<T4>,
-    flow5: Flow<T5>,
-    flow6: Flow<T6>,
-    crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
-): Flow<R> {
-    return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
-        ->
-        @Suppress("UNCHECKED_CAST")
-        transform(
-            args[0] as T1,
-            args[1] as T2,
-            args[2] as T3,
-            args[3] as T4,
-            args[4] as T5,
-            args[5] as T6
-        )
-    }
-}
 
 inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
     flow: Flow<T1>,
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
index a13d85b..cabe831 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
@@ -25,9 +25,13 @@
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.newFixedThreadPoolContext
 import kotlinx.coroutines.plus
 
+private const val LIMIT_BACKGROUND_DISPATCHER_THREADS = true
+
 /** Providers for various SystemIU specific coroutines-related constructs. */
 @Module
 class SysUICoroutinesModule {
@@ -40,14 +44,13 @@
     ): CoroutineScope = applicationScope.plus(coroutineContext)
 
     /**
-     * Provide a [CoroutineDispatcher] backed by a thread pool containing at most X threads, where X
-     * is the number of CPU cores available.
+     * Default Coroutine dispatcher for background operations.
      *
-     * Because there are multiple threads at play, there is no serialization order guarantee. You
-     * should use a [kotlinx.coroutines.channels.Channel] for serialization if necessary.
-     *
-     * @see Dispatchers.Default
+     * Note that this is explicitly limiting the number of threads. In the past, we used
+     * [Dispatchers.IO]. This caused >40 threads to be spawned, and a lot of thread list lock
+     * contention between then, eventually causing jank.
      */
+    @OptIn(DelicateCoroutinesApi::class)
     @Provides
     @SysUISingleton
     @Background
@@ -55,12 +58,29 @@
         "Use @Background CoroutineContext instead",
         ReplaceWith("bgCoroutineContext()", "kotlin.coroutines.CoroutineContext")
     )
-    fun bgDispatcher(): CoroutineDispatcher = Dispatchers.IO
+    fun bgDispatcher(): CoroutineDispatcher {
+        return if (LIMIT_BACKGROUND_DISPATCHER_THREADS) {
+            // Why a new ThreadPool instead of just using Dispatchers.IO with
+            // CoroutineDispatcher.limitedParallelism? Because, if we were to use Dispatchers.IO, we
+            // would share those threads with other dependencies using Dispatchers.IO.
+            // Using a dedicated thread pool we have guarantees only SystemUI is able to schedule
+            // code on those.
+            newFixedThreadPoolContext(
+                nThreads = Runtime.getRuntime().availableProcessors(),
+                name = "SystemUIBg"
+            )
+        } else {
+            Dispatchers.IO
+        }
+    }
 
     @Provides
     @Background
     @SysUISingleton
-    fun bgCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
-        return Dispatchers.IO + tracingCoroutineContext
+    fun bgCoroutineContext(
+        @Tracing tracingCoroutineContext: CoroutineContext,
+        @Background bgCoroutineDispatcher: CoroutineDispatcher,
+    ): CoroutineContext {
+        return bgCoroutineDispatcher + tracingCoroutineContext
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 7c6ad23..e832506 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -32,8 +32,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.inputmethodservice.InputMethodService;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.util.Log;
 import android.view.Display;
@@ -127,7 +125,6 @@
     private final DisplayTracker mDisplayTracker;
     private final NoteTaskInitializer mNoteTaskInitializer;
     private final Executor mSysUiMainExecutor;
-    private HandlerThread mHandlerThread;
 
     // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
     // avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference
@@ -209,8 +206,6 @@
         mDisplayTracker = displayTracker;
         mNoteTaskInitializer = noteTaskInitializer;
         mSysUiMainExecutor = sysUiMainExecutor;
-        mHandlerThread = new HandlerThread("WMShell");
-        mHandlerThread.start();
     }
 
     @Override
@@ -224,8 +219,7 @@
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
 
         // Subscribe to user changes
-        mUserTracker.addCallback(mUserChangedCallback,
-                     new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
 
         mCommandQueue.addCallback(this);
         mPipOptional.ifPresent(this::initPip);
@@ -353,10 +347,10 @@
         desktopMode.addVisibleTasksListener(
                 new DesktopModeTaskRepository.VisibleTasksListener() {
                     @Override
-                    public void onVisibilityChanged(int displayId, boolean hasFreeformTasks) {
+                    public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
                         if (displayId == Display.DEFAULT_DISPLAY) {
                             mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
-                                            hasFreeformTasks)
+                                            visibleTasksCount > 0)
                                     .commitUpdate(mDisplayTracker.getDefaultDisplayId());
                         }
                         // TODO(b/278084491): update sysui state for changes on other displays
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index 44770fa..b23dfdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -16,8 +16,10 @@
 
 package com.android.systemui.accessibility;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.IBinder;
 import android.view.Display;
 import android.view.View;
 import android.view.ViewGroup;
@@ -89,6 +91,11 @@
         return mWindowManager.getMaximumWindowMetrics();
     }
 
+    @Override
+    public @NonNull IBinder getDefaultToken() {
+        return mWindowManager.getDefaultToken();
+    }
+
     public View getAttachedView() {
         return mView;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index d86d123..8299acb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -21,8 +21,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -33,10 +35,16 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
@@ -46,6 +54,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.model.SysUiState;
@@ -63,7 +72,10 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
 
 @LargeTest
 @RunWith(AndroidTestingRunner.class)
@@ -71,6 +83,8 @@
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
     private static final float DEFAULT_SCALE = 4.0f;
     private static final float DEFAULT_CENTER_X = 400.0f;
     private static final float DEFAULT_CENTER_Y = 500.0f;
@@ -107,6 +121,13 @@
 
     private TestableWindowManager mWindowManager;
     private ValueAnimator mValueAnimator;
+    // This list contains all SurfaceControlViewHosts created during a given test. If the
+    // magnification window is recreated during a test, the list will contain more than a single
+    // element.
+    private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+    // The most recently created SurfaceControlViewHost.
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+    private SurfaceControl.Transaction mTransaction;
 
     @Before
     public void setUp() throws Exception {
@@ -123,10 +144,27 @@
         mValueAnimator = newValueAnimator();
         mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
                 mContext, mValueAnimator);
-        mController = new SpyWindowMagnificationController(mContext, mHandler,
+
+        Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+            mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+                    mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+            mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+            return mSurfaceControlViewHost;
+        };
+
+        mTransaction = spy(new SurfaceControl.Transaction());
+        mController = new SpyWindowMagnificationController(
+                mContext,
+                mHandler,
                 mWindowMagnificationAnimationController,
-                mSfVsyncFrameProvider, null, new SurfaceControl.Transaction(),
-                mWindowMagnifierCallback, mSysUiState, mSecureSettings);
+                /* mirrorWindowControl= */ null,
+                mTransaction,
+                mWindowMagnifierCallback,
+                mSysUiState,
+                mSecureSettings,
+                scvhSupplier,
+                mSfVsyncFrameProvider);
+
         mSpyController = mController.getSpyController();
     }
 
@@ -235,8 +273,52 @@
         verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y);
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
     @Test
-    public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback()
+    public void
+            enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOn_AnimationAndCallbackTrue()
+            throws RemoteException {
+        enableWindowMagnificationWithoutAnimation();
+
+        // Wait for Rects updated.
+        waitForIdleSync();
+        View mirrorView = mSurfaceControlViewHost.getView();
+        final float targetScale = 1.0f;
+        // Move the magnifier to the top left corner, within the boundary
+        final float targetCenterX = mirrorView.getWidth() / 2.0f;
+        final float targetCenterY = mirrorView.getHeight() / 2.0f;
+
+        Mockito.reset(mSpyController);
+        getInstrumentation().runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
+
+        verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
+                mScaleCaptor.capture(),
+                mCenterXCaptor.capture(), mCenterYCaptor.capture(),
+                mOffsetXCaptor.capture(), mOffsetYCaptor.capture());
+        verifyStartValue(mScaleCaptor, mCurrentScale.get());
+        verifyStartValue(mCenterXCaptor, mCurrentCenterX.get());
+        verifyStartValue(mCenterYCaptor, mCurrentCenterY.get());
+        verifyStartValue(mOffsetXCaptor, 0f);
+        verifyStartValue(mOffsetYCaptor, 0f);
+
+        verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
+
+        verify(mAnimationCallback).onResult(true);
+        assertEquals(WindowMagnificationAnimationController.STATE_ENABLED,
+                mWindowMagnificationAnimationController.getState());
+    }
+
+    @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+    @Test
+    public void
+            enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOff_AnimationAndCallbackTrue()
             throws RemoteException {
         enableWindowMagnificationWithoutAnimation();
 
@@ -475,8 +557,46 @@
         verify(mAnimationCallback2).onResult(true);
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
     @Test
-    public void enableWindowMagnificationWithOffset_expectedValues() {
+    public void enableWindowMagnificationWithOffset_windowlessFlagOn_expectedValues() {
+        final float offsetRatio = -0.1f;
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+
+        Mockito.reset(mSpyController);
+        getInstrumentation().runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
+                    windowBounds.exactCenterX(), windowBounds.exactCenterY(),
+                    offsetRatio, offsetRatio, mAnimationCallback);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
+        // Wait for Rects update
+        waitForIdleSync();
+
+        final int mirrorSurfaceMargin = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        final int defaultMagnificationWindowSize =
+                mController.getMagnificationWindowSizeFromIndex(
+                        WindowMagnificationSettings.MagnificationSize.MEDIUM);
+        final int defaultMagnificationFrameSize =
+                defaultMagnificationWindowSize - 2 * mirrorSurfaceMargin;
+        final int expectedOffset = (int) (defaultMagnificationFrameSize / 2 * offsetRatio);
+
+        final float expectedX = (int) (windowBounds.exactCenterX() + expectedOffset
+                - defaultMagnificationWindowSize / 2);
+        final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset
+                - defaultMagnificationWindowSize / 2);
+
+        // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is
+        // created and we place the mirrored content as a child of the SurfaceView
+        // (3) the animation starts (4) the animation updates
+        verify(mTransaction, times(4))
+                .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY));
+    }
+
+    @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+    @Test
+    public void enableWindowMagnificationWithOffset_windowlessFlagOff_expectedValues() {
         final float offsetRatio = -0.1f;
         final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
 
@@ -876,23 +996,28 @@
     private static class SpyWindowMagnificationController extends WindowMagnificationController {
         private WindowMagnificationController mSpyController;
 
-        SpyWindowMagnificationController(Context context, Handler handler,
+        SpyWindowMagnificationController(Context context,
+                Handler handler,
                 WindowMagnificationAnimationController animationController,
-                SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
-                MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
-                WindowMagnifierCallback callback, SysUiState sysUiState,
-                SecureSettings secureSettings) {
+                MirrorWindowControl mirrorWindowControl,
+                SurfaceControl.Transaction transaction,
+                WindowMagnifierCallback callback,
+                SysUiState sysUiState,
+                SecureSettings secureSettings,
+                Supplier<SurfaceControlViewHost> scvhSupplier,
+                SfVsyncFrameCallbackProvider sfVsyncFrameProvider) {
             super(
                     context,
                     handler,
                     animationController,
-                    sfVsyncFrameProvider,
                     mirrorWindowControl,
                     transaction,
                     callback,
                     sysUiState,
-                    WindowManagerGlobal::getWindowSession,
-                    secureSettings);
+                    secureSettings,
+                    scvhSupplier,
+                    sfVsyncFrameProvider,
+                    WindowManagerGlobal::getWindowSession);
             mSpyController = Mockito.mock(WindowMagnificationController.class);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 04aef82..2225ad6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -68,6 +68,9 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -90,6 +93,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -121,10 +125,13 @@
 @LargeTest
 @TestableLooper.RunWithLooper
 @RunWith(AndroidTestingRunner.class)
+@RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
 public class WindowMagnificationControllerTest extends SysuiTestCase {
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
     @Mock
@@ -216,13 +223,14 @@
                         mContext,
                         mHandler,
                         mWindowMagnificationAnimationController,
-                        mSfVsyncFrameProvider,
                         mMirrorWindowControl,
                         mTransaction,
                         mWindowMagnifierCallback,
                         mSysUiState,
-                        () -> mWindowSessionSpy,
-                        mSecureSettings);
+                        mSecureSettings,
+                        /* scvhSupplier= */ () -> null,
+                        mSfVsyncFrameProvider,
+                        /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy);
 
         verify(mMirrorWindowControl).setWindowDelegate(
                 any(MirrorWindowControl.MirrorWindowDelegate.class));
@@ -270,7 +278,7 @@
         mInstrumentation.runOnMainSync(
                 () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                         Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
-                /* magnificationFrameOffsetRatioY= */ 0, null));
+                        /* magnificationFrameOffsetRatioY= */ 0, null));
 
         // Waits for the surface created
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
@@ -1415,7 +1423,7 @@
     }
 
     private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
-                                          float y) {
+            float y) {
         return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
     }
 
@@ -1474,4 +1482,4 @@
             });
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
new file mode 100644
index 0000000..66fb63b6c3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -0,0 +1,1502 @@
+/*
+ * Copyright (C) 2024 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.accessibility;
+
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
+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.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowInsets.Type.systemGestures;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalAnswers.returnsSecondArg;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.annotation.IdRes;
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.text.TextUtils;
+import android.util.Size;
+import android.view.AttachedSurfaceControl;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.IRemoteMagnificationAnimationCallback;
+import android.widget.FrameLayout;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+
+import com.android.systemui.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.AnimatorTestRule;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeDisplayTracker;
+import com.android.systemui.util.leak.ReferenceTestUtils;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.os.FakeHandler;
+
+import com.google.common.util.concurrent.AtomicDouble;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+@LargeTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
+
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
+    @Mock
+    private MirrorWindowControl mMirrorWindowControl;
+    @Mock
+    private WindowMagnifierCallback mWindowMagnifierCallback;
+    @Mock
+    IRemoteMagnificationAnimationCallback mAnimationCallback;
+    @Mock
+    IRemoteMagnificationAnimationCallback mAnimationCallback2;
+
+    private SurfaceControl.Transaction mTransaction;
+    @Mock
+    private SecureSettings mSecureSettings;
+
+    private long mWaitAnimationDuration;
+    private long mWaitBounceEffectDuration;
+
+    private Handler mHandler;
+    private TestableWindowManager mWindowManager;
+    private SysUiState mSysUiState;
+    private Resources mResources;
+    private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
+    private WindowMagnificationController mWindowMagnificationController;
+    private Instrumentation mInstrumentation;
+    private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
+    private View mSpyView;
+    private View.OnTouchListener mTouchListener;
+
+    private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+
+    // This list contains all SurfaceControlViewHosts created during a given test. If the
+    // magnification window is recreated during a test, the list will contain more than a single
+    // element.
+    private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+    // The most recently created SurfaceControlViewHost.
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+    private KosmosJavaAdapter mKosmos;
+
+    /**
+     *  return whether window magnification is supported for current test context.
+     */
+    private boolean isWindowModeSupported() {
+        return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mKosmos = new KosmosJavaAdapter(this);
+        mContext = Mockito.spy(getContext());
+        mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        mWindowManager = spy(new TestableWindowManager(wm));
+
+        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        mSysUiState = new SysUiState(mDisplayTracker, mKosmos.getSceneContainerPlugin());
+        mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class));
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then(
+                returnsSecondArg());
+        when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then(
+                returnsSecondArg());
+
+        mResources = getContext().getOrCreateTestableResources().getResources();
+        // prevent the config orientation from undefined, which may cause config.diff method
+        // neglecting the orientation update.
+        if (mResources.getConfiguration().orientation == ORIENTATION_UNDEFINED) {
+            mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT;
+        }
+
+        // Using the animation duration in WindowMagnificationAnimationController for testing.
+        mWaitAnimationDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_longAnimTime);
+        // Using the bounce effect duration in WindowMagnificationController for testing.
+        mWaitBounceEffectDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_shortAnimTime);
+
+        mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
+                mContext, mValueAnimator);
+        Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+            mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+                    mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+            ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+            when(mSurfaceControlViewHost.getRootSurfaceControl()).thenReturn(viewRoot);
+            mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+            return mSurfaceControlViewHost;
+        };
+        mTransaction = spy(new SurfaceControl.Transaction());
+        mWindowMagnificationController =
+                new WindowMagnificationController(
+                        mContext,
+                        mHandler,
+                        mWindowMagnificationAnimationController,
+                        mMirrorWindowControl,
+                        mTransaction,
+                        mWindowMagnifierCallback,
+                        mSysUiState,
+                        mSecureSettings,
+                        scvhSupplier,
+                        /* sfVsyncFrameProvider= */ null,
+                        /* globalWindowSessionSupplier= */ null);
+
+        verify(mMirrorWindowControl).setWindowDelegate(
+                any(MirrorWindowControl.MirrorWindowDelegate.class));
+        mSpyView = Mockito.spy(new View(mContext));
+        doAnswer((invocation) -> {
+            mTouchListener = invocation.getArgument(0);
+            return null;
+        }).when(mSpyView).setOnTouchListener(
+                any(View.OnTouchListener.class));
+
+        // skip test if window magnification is not supported to prevent fail results. (b/279820875)
+        Assume.assumeTrue(isWindowModeSupported());
+    }
+
+    @After
+    public void tearDown() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+        mValueAnimator.cancel();
+    }
+
+    @Test
+    public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() {
+        verify(mSecureSettings).getIntForUser(
+                eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING),
+                /* def */ eq(1), /* userHandle= */ anyInt());
+        assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled());
+    }
+
+    @Test
+    public void enableWindowMagnification_showControlAndNotifyBoundsChanged() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        verify(mMirrorWindowControl).showControl();
+        verify(mWindowMagnifierCallback,
+                timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeastOnce()).onWindowMagnifierBoundsChanged(
+                eq(mContext.getDisplayId()), any(Rect.class));
+    }
+
+    @Test
+    public void enableWindowMagnification_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                        Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
+                        /* magnificationFrameOffsetRatioY= */ 0, null));
+
+        // Waits for the surface created
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), any());
+    }
+
+    @Test
+    public void enableWindowMagnification_disabled_notifySourceBoundsChanged() {
+        enableWindowMagnification_notifySourceBoundsChanged();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification(null));
+        Mockito.reset(mWindowMagnifierCallback);
+
+        enableWindowMagnification_notifySourceBoundsChanged();
+    }
+
+    @Test
+    public void enableWindowMagnification_withAnimation_schedulesFrame() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(2.0f, 10,
+                    10, /* magnificationFrameOffsetRatioX= */ 0,
+                    /* magnificationFrameOffsetRatioY= */ 0,
+                    Mockito.mock(IRemoteMagnificationAnimationCallback.class));
+        });
+        advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS);
+
+        verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+                eq(Surface.ROTATION_0));
+    }
+
+    @Test
+    public void moveWindowMagnifier_enabled_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifier(10, 10);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+    }
+
+    @Test
+    public void enableWindowMagnification_systemGestureExclusionRectsIsSet() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        // Wait for Rects updated.
+        waitForIdleSync();
+
+        List<Rect> rects = mSurfaceControlViewHost.getView().getSystemGestureExclusionRects();
+        assertFalse(rects.isEmpty());
+    }
+
+    @Ignore("The default window size should be constrained after fixing b/288056772")
+    @Test
+    public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() {
+        final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        final int halfScreenSize = screenSize / 2;
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+        // The frame size should be the half of smaller value of window height/width unless it
+        //exceed the max frame size.
+        assertTrue(params.width < halfScreenSize);
+        assertTrue(params.height < halfScreenSize);
+    }
+
+    @Test
+    public void deleteWindowMagnification_destroyControlAndUnregisterComponentCallback() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+
+        verify(mMirrorWindowControl).destroyControl();
+        verify(mContext).unregisterComponentCallbacks(mWindowMagnificationController);
+    }
+
+    @Test
+    public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() {
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    bounds.bottom);
+        });
+        ReferenceTestUtils.waitForCondition(this::hasMagnificationOverlapFlag);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.deleteWindowMagnification();
+        });
+
+        verify(mMirrorWindowControl).destroyControl();
+        assertFalse(hasMagnificationOverlapFlag());
+    }
+
+    @Test
+    public void deleteWindowMagnification_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+
+        // The first time is for notifying magnification enabled and the second time is for
+        // notifying magnification disabled.
+        verify(mWindowMagnifierCallback, times(2)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), any());
+    }
+
+    @Test
+    public void moveMagnifier_schedulesFrame() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        waitForIdleSync();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.moveWindowMagnifier(100f, 100f));
+
+        verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+                eq(Surface.ROTATION_0));
+    }
+
+    @Test
+    public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback()
+            throws RemoteException {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10;
+        final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    targetCenterX, targetCenterY, mAnimationCallback);
+        });
+        advanceTimeBy(mWaitAnimationDuration);
+
+        verify(mAnimationCallback, times(1)).onResult(eq(true));
+        verify(mAnimationCallback, never()).onResult(eq(false));
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+        assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0);
+        assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0);
+    }
+
+    @Test
+    public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback()
+            throws RemoteException {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        final float centerX = sourceBoundsCaptor.getValue().exactCenterX();
+        final float centerY = sourceBoundsCaptor.getValue().exactCenterY();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 10, centerY + 10, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 20, centerY + 20, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 30, centerY + 30, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 40, centerY + 40, mAnimationCallback2);
+        });
+        advanceTimeBy(mWaitAnimationDuration);
+
+        // only the last one callback will return true
+        verify(mAnimationCallback2).onResult(eq(true));
+        // the others will return false
+        verify(mAnimationCallback, times(3)).onResult(eq(false));
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+        assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0);
+        assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0);
+    }
+
+    @Test
+    public void setScale_enabled_expectedValueAndUpdateStateDescription() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f,
+                        Float.NaN, Float.NaN));
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.setScale(3.0f));
+
+        assertEquals(3.0f, mWindowMagnificationController.getScale(), 0);
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertNotNull(mirrorView);
+        assertThat(mirrorView.getStateDescription().toString(), containsString("300"));
+    }
+
+    @Test
+    public void onConfigurationChanged_disabled_withoutException() {
+        Display display = Mockito.spy(mContext.getDisplay());
+        when(display.getRotation()).thenReturn(Surface.ROTATION_90);
+        when(mContext.getDisplay()).thenReturn(display);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+    }
+
+    @Test
+    public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() {
+        final int newRotation = simulateRotateTheDevice();
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        final float center = Math.min(windowBounds.exactCenterX(), windowBounds.exactCenterY());
+        final float displayWidth = windowBounds.width();
+        final PointF magnifiedCenter = new PointF(center, center + 5f);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                    magnifiedCenter.x, magnifiedCenter.y);
+            // Get the center again in case the center we set is out of screen.
+            magnifiedCenter.set(mWindowMagnificationController.getCenterX(),
+                    mWindowMagnificationController.getCenterY());
+        });
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+                ActivityInfo.CONFIG_ORIENTATION));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+        final PointF expectedCenter = new PointF(magnifiedCenter.y,
+                displayWidth - magnifiedCenter.x);
+        final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(),
+                mWindowMagnificationController.getCenterY());
+        assertEquals(expectedCenter, actualCenter);
+    }
+
+    @Test
+    public void onOrientationChanged_disabled_updateDisplayRotation() {
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
+        final int newRotation = simulateRotateTheDevice();
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+                ActivityInfo.CONFIG_ORIENTATION));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+    }
+
+    @Test
+    public void onScreenSizeAndDensityChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() {
+        // The default position is at the center of the screen.
+        final float expectedRatio = 0.5f;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        // Screen size and density change
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        final Rect testWindowBounds = new Rect(
+                mWindowManager.getCurrentWindowMetrics().getBounds());
+        testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+                testWindowBounds.right + 100, testWindowBounds.bottom + 100);
+        mWindowManager.setWindowBounds(testWindowBounds);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        // The ratio of center to window size should be the same.
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterX() / testWindowBounds.width(),
+                0);
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterY() / testWindowBounds.height(),
+                0);
+    }
+
+    @Test
+    public void onScreenChangedToSavedDensity_enabled_restoreSavedMagnifierWindow() {
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        int windowFrameSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        mWindowMagnificationController.mWindowMagnificationSizePrefs.saveSizeForCurrentDensity(
+                new Size(windowFrameSize, windowFrameSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+        assertTrue(params.width == windowFrameSize);
+        assertTrue(params.height == windowFrameSize);
+    }
+
+    @Test
+    public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+        // Screen size and density change
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        final int defaultWindowSize =
+                mWindowMagnificationController.getMagnificationWindowSizeFromIndex(
+                        WindowMagnificationSettings.MagnificationSize.MEDIUM);
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+
+        assertTrue(params.width == defaultWindowSize);
+        assertTrue(params.height == defaultWindowSize);
+    }
+
+    @Test
+    public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            Mockito.reset(mWindowManager);
+            Mockito.reset(mMirrorWindowControl);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+        });
+
+        verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+        verify(mSurfaceControlViewHosts.get(0)).release();
+        verify(mMirrorWindowControl).destroyControl();
+        verify(mSurfaceControlViewHosts.get(1)).setView(any(), any());
+        verify(mMirrorWindowControl).showControl();
+    }
+
+    @Test
+    public void onDensityChanged_disabled_updateDimensions() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+        });
+
+        verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+    }
+
+    @Test
+    public void initializeA11yNode_enabled_expectedValues() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+                    Float.NaN);
+        });
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertNotNull(mirrorView);
+        final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
+
+        mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo);
+
+        assertNotNull(nodeInfo.getContentDescription());
+        assertThat(nodeInfo.getStateDescription().toString(), containsString("250"));
+        assertThat(nodeInfo.getActionList(),
+                hasItems(new AccessibilityAction(R.id.accessibility_action_zoom_in, null),
+                        new AccessibilityAction(R.id.accessibility_action_zoom_out, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_right, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_left, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_down, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_up, null)));
+    }
+
+    @Test
+    public void performA11yActions_visible_expectedResults() {
+        final int displayId = mContext.getDisplayId();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN,
+                    Float.NaN);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null));
+        // Minimum scale is 1.0.
+        verify(mWindowMagnifierCallback).onPerformScaleAction(
+                eq(displayId), /* scale= */ eq(1.0f), /* updatePersistence= */ eq(true));
+
+        assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null));
+        verify(mWindowMagnifierCallback).onPerformScaleAction(
+                eq(displayId), /* scale= */ eq(2.5f), /* updatePersistence= */ eq(true));
+
+        // TODO: Verify the final state when the mirror surface is visible.
+        assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null));
+        verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId));
+
+        assertTrue(mirrorView.performAccessibilityAction(
+                AccessibilityAction.ACTION_CLICK.getId(), null));
+        verify(mWindowMagnifierCallback).onClickSettingsButton(eq(displayId));
+    }
+
+    @Test
+    public void performA11yActions_visible_notifyAccessibilityActionPerformed() {
+        final int displayId = mContext.getDisplayId();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+                    Float.NaN);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null);
+
+        verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId));
+    }
+
+    @Test
+    public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        View closeButton = getInternalView(R.id.close_button);
+        View bottomRightCorner = getInternalView(R.id.bottom_right_corner);
+        View bottomLeftCorner = getInternalView(R.id.bottom_left_corner);
+        View topRightCorner = getInternalView(R.id.top_right_corner);
+        View topLeftCorner = getInternalView(R.id.top_left_corner);
+
+        assertEquals(View.VISIBLE, closeButton.getVisibility());
+        assertEquals(View.VISIBLE, bottomRightCorner.getVisibility());
+        assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility());
+        assertEquals(View.VISIBLE, topRightCorner.getVisibility());
+        assertEquals(View.VISIBLE, topLeftCorner.getVisibility());
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        mInstrumentation.runOnMainSync(() ->
+                mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(),
+                        null));
+
+        assertEquals(View.GONE, closeButton.getVisibility());
+        assertEquals(View.GONE, bottomRightCorner.getVisibility());
+        assertEquals(View.GONE, bottomLeftCorner.getVisibility());
+        assertEquals(View.GONE, topRightCorner.getVisibility());
+        assertEquals(View.GONE, topLeftCorner.getVisibility());
+    }
+
+    @Test
+
+    public void windowWidthIsNotMax_performA11yActionIncreaseWidth_windowWidthIncreased() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = (int) (windowBounds.width() * 0.8);
+        final int startingHeight = (int) (windowBounds.height() * 0.8);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_increase_window_width, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window width includes the magnifier frame and the margin. Increasing the window size
+        // will be increasing the amount of the frame size only.
+        int newWindowWidth =
+                (int) ((startingWidth - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(newWindowWidth, actualWindowWidth.get());
+        assertEquals(startingHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowHeightIsNotMax_performA11yActionIncreaseHeight_windowHeightIncreased() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = (int) (windowBounds.width() * 0.8);
+        final int startingHeight = (int) (windowBounds.height() * 0.8);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_increase_window_height, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window height includes the magnifier frame and the margin. Increasing the window size
+        // will be increasing the amount of the frame size only.
+        int newWindowHeight =
+                (int) ((startingHeight - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(startingWidth, actualWindowWidth.get());
+        assertEquals(newWindowHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowWidthIsMax_noIncreaseWindowWidthA11yAction() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = windowBounds.width();
+        final int startingHeight = windowBounds.height();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_increase_window_width, null)));
+    }
+
+    @Test
+    public void windowHeightIsMax_noIncreaseWindowHeightA11yAction() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = windowBounds.width();
+        final int startingHeight = windowBounds.height();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_increase_window_height, null)));
+    }
+
+    @Test
+    public void windowWidthIsNotMin_performA11yActionDecreaseWidth_windowWidthDecreased() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = (int) (mMinWindowSize * 1.1);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_decrease_window_width, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window width includes the magnifier frame and the margin. Decreasing the window size
+        // will be decreasing the amount of the frame size only.
+        int newWindowWidth =
+                (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(newWindowWidth, actualWindowWidth.get());
+        assertEquals(startingSize, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowHeightIsNotMin_performA11yActionDecreaseHeight_windowHeightDecreased() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = (int) (mMinWindowSize * 1.1);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_decrease_window_height, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window height includes the magnifier frame and the margin. Decreasing the window size
+        // will be decreasing the amount of the frame size only.
+        int newWindowHeight =
+                (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(startingSize, actualWindowWidth.get());
+        assertEquals(newWindowHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowWidthIsMin_noDecreaseWindowWidthA11yAction() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = mMinWindowSize;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_decrease_window_width, null)));
+    }
+
+    @Test
+    public void windowHeightIsMin_noDecreaseWindowHeightA11yAction() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = mMinWindowSize;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_decrease_window_height, null)));
+    }
+
+    @Test
+    public void enableWindowMagnification_hasA11yWindowTitle() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        assertEquals(getContext().getResources().getString(
+                com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle());
+    }
+
+    @Test
+    public void enableWindowMagnificationWithScaleLessThanOne_enabled_disabled() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(0.9f, Float.NaN,
+                    Float.NaN);
+        });
+
+        assertEquals(Float.NaN, mWindowMagnificationController.getScale(), 0);
+    }
+
+    @Test
+    public void enableWindowMagnification_rotationIsChanged_updateRotationValue() {
+        // the config orientation should not be undefined, since it would cause config.diff
+        // returning 0 and thus the orientation changed would not be detected
+        assertNotEquals(ORIENTATION_UNDEFINED, mResources.getConfiguration().orientation);
+
+        final Configuration config = mResources.getConfiguration();
+        config.orientation = config.orientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT
+                : ORIENTATION_LANDSCAPE;
+        final int newRotation = simulateRotateTheDevice();
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+    }
+
+    @Test
+    public void enableWindowMagnification_registerComponentCallback() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        verify(mContext).registerComponentCallbacks(mWindowMagnificationController);
+    }
+
+    @Test
+    public void onLocaleChanged_enabled_updateA11yWindowTitle() {
+        final String newA11yWindowTitle = "new a11y window title";
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        final TestableResources testableResources = getContext().getOrCreateTestableResources();
+        testableResources.addOverride(com.android.internal.R.string.android_system_label,
+                newA11yWindowTitle);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+        });
+
+        assertTrue(TextUtils.equals(newA11yWindowTitle, getAccessibilityWindowTitle()));
+    }
+
+    @Ignore("it's flaky in presubmit but works in abtd, filter for now. b/305654925")
+    @Test
+    public void onSingleTap_enabled_scaleAnimates() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onSingleTap(mSpyView);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+
+        final AtomicDouble maxScaleX = new AtomicDouble();
+        advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> {
+            // For some reason the fancy way doesn't compile...
+            // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max);
+            final double oldMax = maxScaleX.get();
+            final double newMax = Math.max(mirrorView.getScaleX(), oldMax);
+            assertTrue(maxScaleX.compareAndSet(oldMax, newMax));
+        });
+
+        assertTrue(maxScaleX.get() > 1.0);
+    }
+
+    @Test
+    public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifier(0, bounds.height());
+        });
+
+        ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag());
+    }
+
+    @Test
+    public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion()
+            throws RemoteException {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            Float.NaN, Float.NaN, Float.NaN);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+        // Verifying two times in: (1) enable window magnification (2) reposition drag handle
+        verify(viewRoot, times(2)).setTouchableRegion(any());
+
+        View dragButton = getInternalView(R.id.drag_handle);
+        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+        assertEquals(Gravity.BOTTOM | Gravity.LEFT, params.gravity);
+    }
+
+    @Test
+    public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion()
+            throws RemoteException {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            Float.NaN, Float.NaN, Float.NaN);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+        // Verifying one times in: (1) enable window magnification
+        verify(viewRoot).setTouchableRegion(any());
+
+        View dragButton = getInternalView(R.id.drag_handle);
+        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+        assertEquals(Gravity.BOTTOM | Gravity.RIGHT, params.gravity);
+    }
+
+    @Test
+    public void setMinimumWindowSize_enabled_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setMinimumWindowSizeThenEnable_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                    Float.NaN, Float.NaN);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLessThanMin_enabled_minimumWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(minimumWindowSize - 10,
+                    minimumWindowSize - 10);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(minimumWindowSize, actualWindowHeight.get());
+        assertEquals(minimumWindowSize, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(bounds.height(), actualWindowHeight.get());
+        assertEquals(bounds.width(), actualWindowWidth.get());
+    }
+
+    @Test
+    public void changeMagnificationSize_expectedWindowSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final float magnificationScaleLarge = 2.5f;
+        final int initSize = Math.min(bounds.width(), bounds.height()) / 3;
+        final int magnificationSize = (int) (initSize * magnificationScaleLarge)
+                - (int) (initSize * magnificationScaleLarge) % 2;
+
+        final int expectedWindowHeight = magnificationSize;
+        final int expectedWindowWidth = magnificationSize;
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.changeMagnificationSize(
+                            WindowMagnificationSettings.MagnificationSize.LARGE);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragCorner_resizesWindow() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                });
+
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize + 2, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2f);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() {
+
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger magnificationCenterX = new AtomicInteger();
+        final AtomicInteger magnificationCenterY = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize,
+                    minimumWindowSize, bounds.right, bounds.bottom);
+            magnificationCenterX.set((int) mWindowMagnificationController.getCenterX());
+            magnificationCenterY.set((int) mWindowMagnificationController.getCenterY());
+        });
+
+        assertTrue(magnificationCenterX.get() < bounds.right);
+        assertTrue(magnificationCenterY.get() < bounds.bottom);
+    }
+
+    @Test
+    public void performSingleTap_DragHandle() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            1.5f, bounds.centerX(), bounds.centerY());
+                });
+        View dragButton = getInternalView(R.id.drag_handle);
+
+        // Perform a single-tap
+        final long downTime = SystemClock.uptimeMillis();
+        dragButton.dispatchTouchEvent(
+                obtainMotionEvent(downTime, 0, ACTION_DOWN, 100, 100));
+        dragButton.dispatchTouchEvent(
+                obtainMotionEvent(downTime, downTime, ACTION_UP, 100, 100));
+
+        verify(mSurfaceControlViewHost).setView(any(View.class), any());
+    }
+
+    private <T extends View> T getInternalView(@IdRes int idRes) {
+        View mirrorView = mSurfaceControlViewHost.getView();
+        T view = mirrorView.findViewById(idRes);
+        assertNotNull(view);
+        return view;
+    }
+
+    private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
+            float y) {
+        return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
+    }
+
+    private CharSequence getAccessibilityWindowTitle() {
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        if (mirrorView == null) {
+            return null;
+        }
+        WindowManager.LayoutParams layoutParams =
+                (WindowManager.LayoutParams) mirrorView.getLayoutParams();
+        return layoutParams.accessibilityTitle;
+    }
+
+    private boolean hasMagnificationOverlapFlag() {
+        return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0;
+    }
+
+    private void setSystemGestureInsets() {
+        final WindowInsets testInsets = new WindowInsets.Builder()
+                .setInsets(systemGestures(), Insets.of(0, 0, 0, 10))
+                .build();
+        mWindowManager.setWindowInsets(testInsets);
+    }
+
+    private int updateMirrorSurfaceMarginDimension() {
+        return mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+    }
+
+    @Surface.Rotation
+    private int simulateRotateTheDevice() {
+        final Display display = Mockito.spy(mContext.getDisplay());
+        final int currentRotation = display.getRotation();
+        final int newRotation = (currentRotation + 1) % 4;
+        when(display.getRotation()).thenReturn(newRotation);
+        when(mContext.getDisplay()).thenReturn(display);
+        return newRotation;
+    }
+
+    // advance time based on the device frame refresh rate
+    private void advanceTimeBy(long timeDelta) {
+        advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null);
+    }
+
+    // advance time based on the device frame refresh rate, and trigger runnable on each refresh
+    private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) {
+        final float frameRate = mContext.getDisplay().getRefreshRate();
+        final int timeSlot = (int) (1000 / frameRate);
+        int round = (int) Math.ceil((double) timeDelta / timeSlot);
+        for (; round >= 0; round--) {
+            mInstrumentation.runOnMainSync(() -> {
+                mAnimatorTestRule.advanceTimeBy(timeSlot);
+                if (runnableOnEachRefresh != null) {
+                    runnableOnEachRefresh.run();
+                }
+            });
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
index b1e471a..fc34255 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 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.biometrics.domain.interactor
 
 import android.hardware.biometrics.AuthenticateOptions
@@ -5,24 +21,19 @@
 import android.hardware.biometrics.IBiometricContextListener.FoldState
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.AuthController
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.fakeDeviceStateRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
-import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
-import com.android.systemui.unfold.updates.FoldStateProvider
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -30,8 +41,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -41,31 +50,20 @@
 
     @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
 
-    private val testScope = TestScope()
-
-    @Mock private lateinit var foldProvider: FoldStateProvider
-    @Mock private lateinit var authController: AuthController
-    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
-
-    private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
-    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val deviceStateRepository = kosmos.fakeDeviceStateRepository
+    private val udfpsOverlayInteractor = kosmos.udfpsOverlayInteractor
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
 
     private lateinit var interactor: LogContextInteractorImpl
 
     @Before
     fun setup() {
-        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        udfpsOverlayInteractor =
-            UdfpsOverlayInteractor(
-                context,
-                authController,
-                selectedUserInteractor,
-                testScope.backgroundScope,
-            )
         interactor =
             LogContextInteractorImpl(
                 testScope.backgroundScope,
-                foldProvider,
+                deviceStateRepository,
                 KeyguardTransitionInteractorFactory.create(
                         repository = keyguardTransitionRepository,
                         scope = testScope.backgroundScope,
@@ -189,33 +187,31 @@
     @Test
     fun foldStateChanges() =
         testScope.runTest {
-            val foldState = collectLastValue(interactor.foldState)
-            runCurrent()
-            val listener = foldProvider.captureListener()
+            val foldState by collectLastValue(interactor.foldState)
 
-            listener.onFoldUpdate(FOLD_UPDATE_START_OPENING)
-            assertThat(foldState()).isEqualTo(FoldState.UNKNOWN)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.HALF_FOLDED)
+            assertThat(foldState).isEqualTo(FoldState.HALF_OPENED)
 
-            listener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
-            assertThat(foldState()).isEqualTo(FoldState.HALF_OPENED)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY)
+            assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
 
-            listener.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
-            assertThat(foldState()).isEqualTo(FoldState.FULLY_OPENED)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
+            assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
 
-            listener.onFoldUpdate(FOLD_UPDATE_START_CLOSING)
-            assertThat(foldState()).isEqualTo(FoldState.FULLY_OPENED)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.FOLDED)
+            assertThat(foldState).isEqualTo(FoldState.FULLY_CLOSED)
 
-            listener.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
-            assertThat(foldState()).isEqualTo(FoldState.FULLY_CLOSED)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.REAR_DISPLAY)
+            assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
+
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNKNOWN)
+            assertThat(foldState).isEqualTo(FoldState.UNKNOWN)
         }
 
     @Test
     fun contextSubscriberChanges() =
         testScope.runTest {
-            runCurrent()
-            val foldListener = foldProvider.captureListener()
-            foldListener.onFoldUpdate(FOLD_UPDATE_START_CLOSING)
-            foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.FOLDED)
             keyguardTransitionRepository.startTransitionTo(KeyguardState.AOD)
 
             var folded: Int? = null
@@ -243,8 +239,7 @@
             assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_AOD)
             assertThat(ignoreTouches).isFalse()
 
-            foldListener.onFoldUpdate(FOLD_UPDATE_START_OPENING)
-            foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.HALF_FOLDED)
             keyguardTransitionRepository.startTransitionTo(KeyguardState.LOCKSCREEN)
             runCurrent()
 
@@ -259,7 +254,7 @@
             job.cancel()
 
             // stale updates should be ignored
-            foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
+            deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
             keyguardTransitionRepository.startTransitionTo(KeyguardState.AOD)
             runCurrent()
 
@@ -270,8 +265,3 @@
 
 private suspend fun FakeKeyguardTransitionRepository.startTransitionTo(newState: KeyguardState) =
     sendTransitionStep(TransitionStep(to = newState, transitionState = TransitionState.STARTED))
-
-private fun FoldStateProvider.captureListener() =
-    withArgCaptor<FoldStateProvider.FoldUpdatesListener> {
-        verify(this@captureListener).addCallback(capture())
-    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 9fe40d7..8b16da2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -46,6 +47,10 @@
     private lateinit var underTest: KeyguardBlueprintInteractor
     private lateinit var testScope: TestScope
 
+    val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
+    val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+
     @Mock private lateinit var splitShadeStateController: SplitShadeStateController
     @Mock private lateinit var keyguardBlueprintRepository: KeyguardBlueprintRepository
 
@@ -54,6 +59,9 @@
         MockitoAnnotations.initMocks(this)
         testScope = TestScope(StandardTestDispatcher())
         whenever(keyguardBlueprintRepository.configurationChange).thenReturn(configurationFlow)
+        whenever(keyguardBlueprintRepository.refreshBluePrint).thenReturn(refreshBluePrint)
+        whenever(keyguardBlueprintRepository.refreshBlueprintTransition)
+            .thenReturn(refreshBlueprintTransition)
 
         underTest =
             KeyguardBlueprintInteractor(
@@ -105,4 +113,11 @@
         underTest.transitionToBlueprint("abc")
         verify(keyguardBlueprintRepository).applyBlueprint("abc")
     }
+
+    @Test
+    fun testRefreshBlueprintWithTransition() {
+        underTest.refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+        verify(keyguardBlueprintRepository)
+            .refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index e93ad0be3..2812718 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -1895,6 +1895,37 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    fun glanceableHubToDreaming() =
+        testScope.runTest {
+            // GIVEN a device that is not dreaming or dozing
+            keyguardRepository.setDreamingWithOverlay(false)
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            runCurrent()
+
+            // GIVEN a prior transition has run to GLANCEABLE_HUB
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+            // WHEN the device begins to dream
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            // THEN a transition to DREAMING should occur
+            assertThat(info.ownerName)
+                .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
     private fun createKeyguardInteractor(): KeyguardInteractor {
         return KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index acb6ff0..2da74b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -165,7 +165,7 @@
         val cs = ConstraintSet()
         underTest.applyDefaultConstraints(cs)
 
-        val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - CLOCK_FADE_TRANSLATION_Y
+        val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
         assetLargeClockTop(cs, expectedLargeClockTopMargin)
 
         val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE
@@ -178,7 +178,7 @@
         setSplitShade(false)
         val cs = ConstraintSet()
         underTest.applyDefaultConstraints(cs)
-        val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - CLOCK_FADE_TRANSLATION_Y
+        val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
         assetLargeClockTop(cs, expectedLargeClockTopMargin)
 
         val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index 75994da..ad2ae8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -65,7 +65,7 @@
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val shadeRepository = kosmos.shadeRepository
     private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
-    private val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+    private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
     private val underTest = kosmos.bouncerToGoneFlows
 
     @Before
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 ba7927d..239bf65 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
@@ -532,6 +532,58 @@
         }
 
     @Test
+    fun testCommunalLocation_showsOverLockscreen() =
+        testScope.runTest {
+            // Device is on lock screen.
+            whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+            // UMO goes to communal even over the lock screen.
+            communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+            runCurrent()
+            verify(mediaCarouselController)
+                .onDesiredLocationChanged(
+                    eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+                    nullable(),
+                    eq(false),
+                    anyLong(),
+                    anyLong()
+                )
+        }
+
+    @Test
+    fun testCommunalLocation_showsUntilQsExpands() =
+        testScope.runTest {
+            // Device is on lock screen.
+            whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+            communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+            runCurrent()
+            verify(mediaCarouselController)
+                .onDesiredLocationChanged(
+                    eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+                    nullable(),
+                    eq(false),
+                    anyLong(),
+                    anyLong()
+                )
+            clearInvocations(mediaCarouselController)
+
+            // Start opening the shade.
+            mediaHierarchyManager.qsExpansion = 0.1f
+            runCurrent()
+
+            // UMO goes to the shade instead.
+            verify(mediaCarouselController)
+                .onDesiredLocationChanged(
+                    eq(MediaHierarchyManager.LOCATION_QS),
+                    any(MediaHostState::class.java),
+                    eq(false),
+                    anyLong(),
+                    anyLong()
+                )
+        }
+
+    @Test
     fun testQsExpandedChanged_noQqsMedia() {
         // When we are looking at QQS with active media
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index a92111e..da8d29c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -246,8 +246,9 @@
 
 
     @Test
-    public void testShouldUzeHorizontalLayout_falseForSplitShade() {
+    public void testShouldUseHorizontalLayout_falseForSplitShade() {
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES;
         when(mMediaHost.getVisible()).thenReturn(true);
 
         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
@@ -270,12 +271,13 @@
     }
 
     @Test
-    public void testChangeConfiguration_shouldUseHorizontalLayout() {
+    public void testChangeConfiguration_shouldUseHorizontalLayoutInLandscape_true() {
         when(mMediaHost.getVisible()).thenReturn(true);
         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
 
-        // When device is rotated to landscape
+        // When device is rotated to landscape and is long
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES;
         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
 
         // Then the layout changes
@@ -292,6 +294,29 @@
     }
 
     @Test
+    public void testChangeConfiguration_shouldUseHorizontalLayoutInLongDevices_true() {
+        when(mMediaHost.getVisible()).thenReturn(true);
+        mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
+
+        // When device is rotated to landscape and is long
+        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES;
+        mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
+
+        // Then the layout changes
+        assertThat(mController.shouldUseHorizontalLayout()).isTrue();
+        verify(mHorizontalLayoutListener).run();
+
+        // When device changes to not-long
+        mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_NO;
+        mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
+
+        // Then the layout changes back
+        assertThat(mController.shouldUseHorizontalLayout()).isFalse();
+        verify(mHorizontalLayoutListener, times(2)).run();
+    }
+
+    @Test
     public void testRefreshAllTilesDoesntRefreshListeningTiles() {
         when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
         mController.setTiles();
@@ -310,6 +335,7 @@
         when(mMediaHost.getVisible()).thenReturn(true);
         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES;
         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index b3e386e..cc27cbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -189,7 +189,6 @@
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
-import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
 import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
@@ -411,7 +410,7 @@
 
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
-                new FakeDeviceProvisioningRepository(),
+                mKosmos.getDeviceProvisioningInteractor(),
                 new FakeDisableFlagsRepository(),
                 mDozeParameters,
                 mFakeKeyguardRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 461db8e..7f4508a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -99,7 +99,6 @@
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
-import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
 import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
@@ -260,7 +259,7 @@
 
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
-                new FakeDeviceProvisioningRepository(),
+                mKosmos.getDeviceProvisioningInteractor(),
                 new FakeDisableFlagsRepository(),
                 mock(DozeParameters.class),
                 keyguardRepository,
@@ -452,11 +451,11 @@
     }
 
     @Test
-    public void setCommunalShowing_userTimeout() {
+    public void setCommunalVisible_userTimeout() {
         setKeyguardShowing();
         clearInvocations(mWindowManager);
 
-        mNotificationShadeWindowController.onCommunalShowingChanged(true);
+        mNotificationShadeWindowController.onCommunalVisibleChanged(true);
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
         assertThat(mLayoutParameters.getValue().userActivityTimeout)
                 .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 3e0a647..7bd9d92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -103,7 +103,6 @@
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
-import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
 import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
@@ -205,9 +204,7 @@
         mStatusBarStateController = mKosmos.getStatusBarStateController();
         mInteractionJankMonitor = mKosmos.getInteractionJankMonitor();
 
-        FakeDeviceProvisioningRepository deviceProvisioningRepository =
-                new FakeDeviceProvisioningRepository();
-        deviceProvisioningRepository.setDeviceProvisioned(true);
+        mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true);
         FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
 
@@ -294,7 +291,7 @@
 
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
-                deviceProvisioningRepository,
+                mKosmos.getDeviceProvisioningInteractor(),
                 mDisableFlagsRepository,
                 mDozeParameters,
                 mKeyguardRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 190ee81..460892a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -49,7 +49,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 260bef8..5da3a56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 
 import android.content.ComponentName;
 import android.graphics.Rect;
@@ -32,11 +33,14 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.view.KeyEvent;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
 
 import androidx.test.filters.SmallTest;
 
@@ -365,14 +369,50 @@
     }
 
     @Test
-    public void testAddQsTile() {
+    @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void addQsTile_withA11yQsShortcutFlagOff() {
         ComponentName c = new ComponentName("testpkg", "testcls");
+
         mCommandQueue.addQsTile(c);
         waitForIdleSync();
+
         verify(mCallbacks).addQsTile(eq(c));
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void addQsTileToFrontOrEnd_withA11yQsShortcutFlagOff_doNothing() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mCommandQueue.addQsTileToFrontOrEnd(c, true);
+        waitForIdleSync();
+
+        verifyZeroInteractions(mCallbacks);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void addQsTile_withA11yQsShortcutFlagOn() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mCommandQueue.addQsTile(c);
+        waitForIdleSync();
+
+        verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(false));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void addQsTileAtTheEnd_withA11yQsShortcutFlagOn() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mCommandQueue.addQsTileToFrontOrEnd(c, true);
+        waitForIdleSync();
+
+        verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(true));
+    }
+
+    @Test
     public void testRemoveQsTile() {
         ComponentName c = new ComponentName("testpkg", "testcls");
         mCommandQueue.remQsTile(c);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 8cb064d..5450537 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -56,8 +56,8 @@
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import kotlinx.coroutines.flow.emptyFlow
@@ -192,7 +192,7 @@
         shadeInteractor =
             ShadeInteractorImpl(
                 testScope.backgroundScope,
-                FakeDeviceProvisioningRepository(),
+                kosmos.deviceProvisioningInteractor,
                 FakeDisableFlagsRepository(),
                 mock(),
                 keyguardRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 912c27d..ea4ae17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -26,6 +26,7 @@
 
 import android.app.ActivityManager;
 import android.app.StatusBarManager;
+import android.content.ComponentName;
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.os.Vibrator;
@@ -190,4 +191,31 @@
                 HapticFeedbackConstants.GESTURE_START
         );
     }
+
+    @Test
+    public void addQsTile_delegateCallToQsHost() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mSbcqCallbacks.addQsTile(c);
+
+        verify(mQSHost).addTile(c);
+    }
+
+    @Test
+    public void addQsTileToFrontOrEnd_toTheEnd_delegateCallToQsHost() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mSbcqCallbacks.addQsTileToFrontOrEnd(c, true);
+
+        verify(mQSHost).addTile(c, true);
+    }
+
+    @Test
+    public void addQsTileToFrontOrEnd_toTheFront_delegateCallToQsHost() {
+        ComponentName c = new ComponentName("testpkg", "testcls");
+
+        mSbcqCallbacks.addQsTileToFrontOrEnd(c, false);
+
+        verify(mQSHost).addTile(c, false);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 9c4984e..849a13b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -41,6 +41,8 @@
 
 import static java.util.Collections.emptySet;
 
+import static kotlinx.coroutines.flow.FlowKt.flowOf;
+
 import android.app.ActivityManager;
 import android.app.IWallpaperManager;
 import android.app.WallpaperManager;
@@ -77,7 +79,6 @@
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.InitController;
 import com.android.systemui.SysuiTestCase;
@@ -92,6 +93,10 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.data.repository.CommunalRepository;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalSceneKey;
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -102,6 +107,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.notetask.NoteTaskController;
@@ -195,6 +201,8 @@
 
 import javax.inject.Provider;
 
+import kotlinx.coroutines.test.TestScope;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -203,11 +211,17 @@
     private static final int FOLD_STATE_FOLDED = 0;
     private static final int FOLD_STATE_UNFOLDED = 1;
 
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+
     private CentralSurfacesImpl mCentralSurfaces;
     private FakeMetricsLogger mMetricsLogger;
     private PowerManager mPowerManager;
     private VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider;
 
+
+    private final TestScope mTestScope = mKosmos.getTestScope();
+    private final CommunalInteractor mCommunalInteractor = mKosmos.getCommunalInteractor();
+    private final CommunalRepository mCommunalRepository = mKosmos.getCommunalRepository();
     @Mock private NotificationsController mNotificationsController;
     @Mock private LightBarController mLightBarController;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -461,7 +475,7 @@
                 new DisplayMetrics(),
                 mMetricsLogger,
                 mShadeLogger,
-                new JavaAdapter(TestScopeProvider.getTestScope()),
+                new JavaAdapter(mTestScope),
                 mUiBgExecutor,
                 mNotificationPanelViewController,
                 mNotificationMediaManager,
@@ -473,6 +487,7 @@
                 mScreenLifecycle,
                 mWakefulnessLifecycle,
                 mPowerInteractor,
+                mCommunalInteractor,
                 mStatusBarStateController,
                 Optional.of(mBubbles),
                 () -> mNoteTaskController,
@@ -821,6 +836,25 @@
     }
 
     @Test
+    public void testEnteringGlanceableHub_updatesScrim() {
+        // Transition to the glanceable hub.
+        mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+                CommunalSceneKey.Communal.INSTANCE)));
+        mTestScope.getTestScheduler().runCurrent();
+
+        // ScrimState also transitions.
+        verify(mScrimController).transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Transition away from the glanceable hub.
+        mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+                CommunalSceneKey.Blank.INSTANCE)));
+        mTestScope.getTestScheduler().runCurrent();
+
+        // ScrimState goes back to UNLOCKED.
+        verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
+    }
+
+    @Test
     public void testShowKeyguardImplementation_setsState() {
         when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>());
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 423cc84..3bde6e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -51,6 +51,7 @@
 import android.graphics.Color;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.testing.ViewUtils;
 import android.util.MathUtils;
 import android.view.View;
 
@@ -59,13 +60,13 @@
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -73,6 +74,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator;
@@ -103,7 +105,6 @@
 import java.util.HashSet;
 import java.util.Map;
 
-import kotlinx.coroutines.CoroutineDispatcher;
 import kotlinx.coroutines.test.TestScope;
 
 @RunWith(AndroidTestingRunner.class)
@@ -112,13 +113,14 @@
 public class ScrimControllerTest extends SysuiTestCase {
 
     @Rule public Expect mExpect = Expect.create();
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
 
     private final FakeConfigurationController mConfigurationController =
             new FakeConfigurationController();
     private final LargeScreenShadeInterpolator
             mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
 
-    private final TestScope mTestScope = TestScopeProvider.getTestScope();
+    private final TestScope mTestScope = mKosmos.getTestScope();
     private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
 
     private ScrimController mScrimController;
@@ -145,10 +147,12 @@
     @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
     @Mock private AlternateBouncerToGoneTransitionViewModel
             mAlternateBouncerToGoneTransitionViewModel;
-    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
+            mKosmos.getKeyguardTransitionInteractor();
+    private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
+            mKosmos.getKeyguardTransitionRepository();
     @Mock private KeyguardInteractor mKeyguardInteractor;
     private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
-    @Mock private CoroutineDispatcher mMainDispatcher;
     @Mock private TypedArray mMockTypedArray;
 
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
@@ -265,8 +269,6 @@
         when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock);
         when(mDockManager.isDocked()).thenReturn(false);
 
-        when(mKeyguardTransitionInteractor.transition(any(), any()))
-                .thenReturn(emptyFlow());
         when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha())
                 .thenReturn(emptyFlow());
         when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
@@ -292,13 +294,16 @@
                 mKeyguardTransitionInteractor,
                 mKeyguardInteractor,
                 mWallpaperRepository,
-                mMainDispatcher,
+                mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator);
         mScrimController.start();
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
 
+        // Attach behind scrim so flows that are collecting on it start running.
+        ViewUtils.attachView(mScrimBehind);
+
         mScrimController.setHasBackdrop(false);
 
         mWallpaperRepository.getWallpaperSupportsAmbientMode().setValue(false);
@@ -629,6 +634,164 @@
     }
 
     @Test
+    public void lockscreenToHubTransition_setsBehindScrimAlpha() {
+        // Start on lockscreen.
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        finishAnimationsImmediately();
+
+        // Behind scrim starts at default alpha.
+        final float transitionProgress = 0f;
+        float expectedAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        transitionProgress,
+                        TransitionState.STARTED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim fades out as transition runs.
+        final float runningProgress = 0.2f;
+        expectedAlpha = (1 - runningProgress) * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        runningProgress,
+                        TransitionState.RUNNING
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim invisible at end of transition.
+        final float finishedProgress = 1f;
+        expectedAlpha = 0f;
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        finishedProgress,
+                        TransitionState.FINISHED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+    }
+
+    @Test
+    public void hubToLockscreenTransition_setsViewAlpha() {
+        // Start on glanceable hub.
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // Behind scrim starts at 0 alpha.
+        final float transitionProgress = 0f;
+        float expectedAlpha = 0f;
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        transitionProgress,
+                        TransitionState.STARTED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim fades in as transition runs.
+        final float runningProgress = 0.2f;
+        expectedAlpha = runningProgress * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        runningProgress,
+                        TransitionState.RUNNING
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim at default visibility at end of transition.
+        final float finishedProgress = 1f;
+        expectedAlpha = finishedProgress * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        finishedProgress,
+                        TransitionState.FINISHED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+    }
+
+    @Test
+    public void transitionToHub() {
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims transparent on the hub.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
+    public void openBouncerOnHub() {
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Open the bouncer.
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_VISIBLE);
+        finishAnimationsImmediately();
+
+        // Only behind widget is visible.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+
+        // Bouncer is closed.
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims are transparent.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
+    public void openShadeOnHub() {
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Open the shade.
+        mScrimController.transitionTo(SHADE_LOCKED);
+        mScrimController.setQsPosition(1f, 0);
+        finishAnimationsImmediately();
+
+        // Shade scrims are visible.
+        assertScrimAlpha(Map.of(
+                mNotificationsScrim, OPAQUE,
+                mScrimInFront, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims are transparent.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
     public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
         assertEquals(BOUNCER.getBehindTint(), 0x112233);
         mSurfaceColor = 0x223344;
@@ -1001,7 +1164,7 @@
                 mKeyguardTransitionInteractor,
                 mKeyguardInteractor,
                 mWallpaperRepository,
-                mMainDispatcher,
+                mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator);
         mScrimController.start();
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
@@ -1267,7 +1430,7 @@
                 ScrimState.UNINITIALIZED, ScrimState.KEYGUARD, BOUNCER,
                 ScrimState.DREAMING, ScrimState.BOUNCER_SCRIMMED, ScrimState.BRIGHTNESS_MIRROR,
                 ScrimState.UNLOCKED, SHADE_LOCKED, ScrimState.AUTH_SCRIMMED,
-                ScrimState.AUTH_SCRIMMED_SHADE));
+                ScrimState.AUTH_SCRIMMED_SHADE, ScrimState.GLANCEABLE_HUB));
 
         for (ScrimState state : ScrimState.values()) {
             if (!lowPowerModeStates.contains(state) && !regularStates.contains(state)) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 21c038a..f53fc46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
@@ -36,6 +37,7 @@
 class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
     private lateinit var underTest: DeviceBasedSatelliteViewModel
     private lateinit var interactor: DeviceBasedSatelliteInteractor
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
 
     private val repo = FakeDeviceBasedSatelliteRepository()
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
@@ -45,6 +47,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
 
         interactor =
             DeviceBasedSatelliteInteractor(
@@ -57,6 +60,7 @@
             DeviceBasedSatelliteViewModel(
                 interactor,
                 testScope.backgroundScope,
+                airplaneModeRepository,
             )
     }
 
@@ -72,6 +76,9 @@
             val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
             i1.isInService.value = false
 
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
             // THEN icon is null because we should not be showing it
             assertThat(latest).isNull()
         }
@@ -88,11 +95,33 @@
             val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
             i1.isInService.value = true
 
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
             // THEN icon is null because we have service
             assertThat(latest).isNull()
         }
 
     @Test
+    fun icon_nullWhenShouldNotShow_apmIsEnabled() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // GIVEN apm is enabled
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            // THEN icon is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
     fun icon_satelliteIsOff() =
         testScope.runTest {
             val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 457acd2..b58a41c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -190,7 +190,7 @@
 
         mWakefulnessLifecycle.dispatchFinishedWakingUp();
         mThemeOverlayController.start();
-        verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor));
+        verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor));
         verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null),
                 eq(UserHandle.USER_ALL));
         verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index abfff34..0669cb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.launchIn
@@ -46,6 +47,7 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class UserRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7eba3f0..9ea4142 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -494,7 +494,7 @@
         mShadeInteractor =
                 new ShadeInteractorImpl(
                         mTestScope.getBackgroundScope(),
-                        deviceProvisioningRepository,
+                        mKosmos.getDeviceProvisioningInteractor(),
                         new FakeDisableFlagsRepository(),
                         mDozeParameters,
                         keyguardRepository,
@@ -2098,6 +2098,26 @@
     }
 
     @Test
+    public void testShowOrHideAppBubble_addsFromOverflow() {
+        String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0);
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+
+        // Collapse the stack so we don't need to wait for the dismiss animation in the test
+        mBubbleController.collapseStack();
+
+        // Dismiss the app bubble so it's in the overflow
+        mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull();
+
+        // Calling this while collapsed will re-add and expand the app bubble
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
+        assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+    }
+
+    @Test
     public void testCreateBubbleFromOngoingNotification() {
         NotificationEntry notif = new NotificationEntryBuilder()
                 .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
@@ -2217,7 +2237,8 @@
 
         FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
         mBubbleController.registerBubbleStateListener(bubbleStateListener);
-        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 500, 1000);
+        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(),
+                new Rect(500, 1000, 600, 1100));
 
         assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
 
diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
new file mode 100644
index 0000000..8901314
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content
+
+import android.content.pm.PackageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
index 06b6cda6..244ef8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
@@ -16,7 +16,40 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
+import android.content.applicationContext
+import com.android.keyguard.keyguardSecurityModel
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.trustRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.concurrency.mockExecutorHandler
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.primaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.mockPrimaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.primaryBouncerInteractor by
+    Kosmos.Fixture {
+        PrimaryBouncerInteractor(
+            repository = keyguardBouncerRepository,
+            primaryBouncerView = mock<BouncerView>(),
+            mainHandler = mockExecutorHandler(executor = fakeExecutor),
+            keyguardStateController = mock<KeyguardStateControllerImpl>(),
+            keyguardSecurityModel = keyguardSecurityModel,
+            primaryBouncerCallbackInteractor = mock<PrimaryBouncerCallbackInteractor>(),
+            falsingCollector = falsingCollector,
+            dismissCallbackRegistry = mock<DismissCallbackRegistry>(),
+            context = applicationContext,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            trustRepository = trustRepository,
+            applicationScope = applicationCoroutineScope,
+            selectedUserInteractor = selectedUserInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index d91c597..99dfe94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.systemClock
@@ -36,8 +38,10 @@
         applicationScope = testScope.backgroundScope,
         mainDispatcher = testDispatcher,
         bouncerInteractor = bouncerInteractor,
+        inputMethodInteractor = inputMethodInteractor,
         simBouncerInteractor = simBouncerInteractor,
         authenticationInteractor = authenticationInteractor,
+        selectedUserInteractor = selectedUserInteractor,
         flags = sceneContainerFlags,
         selectedUser = userSwitcherViewModel.selectedUser,
         users = userSwitcherViewModel.users,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index bc7e7af..fab64e3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -36,6 +36,14 @@
         }
     }
 
+    override fun deleteWidget(widgetId: Int) {
+        if (_communalWidgets.value.none { it.appWidgetId == widgetId }) {
+            return
+        }
+
+        _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId }
+    }
+
     private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
         _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
index 002862e..a231212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.panels
 
 import android.os.UserHandle
+import com.android.systemui.kosmos.Kosmos
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -68,3 +69,6 @@
         }
     }
 }
+
+val Kosmos.selectedComponentRepository by
+    Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index 0b1fb40..5575b05 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -23,7 +23,7 @@
 import com.android.keyguard.trustManager
 import com.android.systemui.biometrics.data.repository.facePropertyRepository
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
@@ -46,7 +46,7 @@
             applicationScope = applicationCoroutineScope,
             mainDispatcher = testDispatcher,
             repository = deviceEntryFaceAuthRepository,
-            primaryBouncerInteractor = { primaryBouncerInteractor },
+            primaryBouncerInteractor = { mockPrimaryBouncerInteractor },
             alternateBouncerInteractor = alternateBouncerInteractor,
             keyguardTransitionInteractor = keyguardTransitionInteractor,
             faceAuthenticationLogger = faceAuthLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
new file mode 100644
index 0000000..2fead91
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.deviceentry.ui.binder
+
+import android.content.packageManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.util.sensors.asyncSensorManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.liftToRunFaceAuthBinder by
+    Kosmos.Fixture {
+        LiftToRunFaceAuthBinder(
+            scope = applicationCoroutineScope,
+            packageManager = packageManager,
+            asyncSensorManager = asyncSensorManager,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            keyguardInteractor = keyguardInteractor,
+            primaryBouncerInteractor = primaryBouncerInteractor,
+            alternateBouncerInteractor = alternateBouncerInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+            powerInteractor = powerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt
new file mode 100644
index 0000000..5855060
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.deviceStateRepository: DeviceStateRepository by
+    Kosmos.Fixture { fakeDeviceStateRepository }
+val Kosmos.fakeDeviceStateRepository: FakeDeviceStateRepository by
+    Kosmos.Fixture { FakeDeviceStateRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
new file mode 100644
index 0000000..8e4461d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.flowOf
+
+@SysUISingleton
+class FakeInputMethodRepository : InputMethodRepository {
+
+    private var usersToEnabledInputMethods: MutableMap<Int, Flow<InputMethodModel>> = mutableMapOf()
+
+    var selectedInputMethodSubtypes = listOf<InputMethodModel.Subtype>()
+
+    /**
+     * The display ID on which the input method picker dialog was shown, or `null` if the dialog was
+     * not shown.
+     */
+    var inputMethodPickerShownDisplayId: Int? = null
+
+    fun setEnabledInputMethods(userId: Int, vararg enabledInputMethods: InputMethodModel) {
+        usersToEnabledInputMethods[userId] = enabledInputMethods.asFlow()
+    }
+
+    override suspend fun enabledInputMethods(
+        userId: Int,
+        fetchSubtypes: Boolean,
+    ): Flow<InputMethodModel> {
+        return usersToEnabledInputMethods[userId] ?: flowOf()
+    }
+
+    override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> =
+        selectedInputMethodSubtypes
+
+    override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        inputMethodPickerShownDisplayId = displayId
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
new file mode 100644
index 0000000..b71b9d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.inputMethodRepository: InputMethodRepository by
+    Kosmos.Fixture { fakeInputMethodRepository }
+val Kosmos.fakeInputMethodRepository by Kosmos.Fixture { FakeInputMethodRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
new file mode 100644
index 0000000..da77575
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.inputmethod.domain.interactor
+
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.inputMethodInteractor by
+    Kosmos.Fixture {
+        InputMethodInteractor(
+            repository = inputMethodRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 0c1dbfe..e20a0ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -28,9 +28,12 @@
 import java.util.UUID
 import javax.inject.Inject
 import junit.framework.Assert.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -150,6 +153,15 @@
         _transitions.emit(step)
     }
 
+    /** Version of [sendTransitionStep] that's usable from Java tests. */
+    fun sendTransitionStepJava(
+        coroutineScope: CoroutineScope,
+        step: TransitionStep,
+        validateStep: Boolean = true
+    ): Job {
+        return coroutineScope.launch { sendTransitionStep(step, validateStep) }
+    }
+
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
index c71c1c3..ffa4133 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
@@ -18,7 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
@@ -31,7 +31,7 @@
 val Kosmos.bouncerToGoneFlows by Fixture {
     BouncerToGoneFlows(
         statusBarStateController = sysuiStatusBarStateController,
-        primaryBouncerInteractor = primaryBouncerInteractor,
+        primaryBouncerInteractor = mockPrimaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
         featureFlags = featureFlagsClassic,
         shadeInteractor = shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
index ab28d0d6..4ecff73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
@@ -30,7 +30,7 @@
 val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture {
     PrimaryBouncerToGoneTransitionViewModel(
         statusBarStateController = sysuiStatusBarStateController,
-        primaryBouncerInteractor = primaryBouncerInteractor,
+        primaryBouncerInteractor = mockPrimaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
         featureFlags = featureFlagsClassic,
         bouncerToGoneFlows = bouncerToGoneFlows,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 11f2938..b9a3d38 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.model.sceneContainerPlugin
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.power.data.repository.fakePowerRepository
@@ -41,6 +43,8 @@
 import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -61,6 +65,8 @@
     val bouncerRepository by lazy { kosmos.bouncerRepository }
     val communalRepository by lazy { kosmos.fakeCommunalRepository }
     val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
     val powerRepository by lazy { kosmos.fakePowerRepository }
     val clock by lazy { kosmos.systemClock }
     val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
@@ -76,6 +82,8 @@
     val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
     val communalInteractor by lazy { kosmos.communalInteractor }
     val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin }
+    val deviceProvisioningInteractor by lazy { kosmos.deviceProvisioningInteractor }
+    val fakeDeviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
 
     init {
         kosmos.applicationContext = testCase.context
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt
new file mode 100644
index 0000000..9410ce6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.fontscaling
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsFontScalingTileConfig by
+    Kosmos.Fixture { QSAccessibilityModule.provideFontScalingTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index afd37b3..2bd76be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -29,8 +29,8 @@
 import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
-import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.user.domain.interactor.userSwitcherInteractor
 
 var Kosmos.baseShadeInteractor: BaseShadeInteractor by
@@ -63,7 +63,7 @@
     Kosmos.Fixture {
         ShadeInteractorImpl(
             scope = applicationCoroutineScope,
-            deviceProvisioningRepository = deviceProvisioningRepository,
+            deviceProvisioningInteractor = deviceProvisioningInteractor,
             disableFlagsRepository = disableFlagsRepository,
             dozeParams = dozeParameters,
             keyguardRepository = fakeKeyguardRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
index f2f3a5a..d79633a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
@@ -25,5 +26,6 @@
     NotificationStackAppearanceViewModel(
         stackAppearanceInteractor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
+        sceneInteractor = sceneInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 7c398cd..549a775 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -20,7 +20,9 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
@@ -40,6 +42,8 @@
         communalInteractor = communalInteractor,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
+        dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
+        lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
         aodBurnInViewModel = aodBurnInViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt
new file mode 100644
index 0000000..84bd3e8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository
+
+val Kosmos.deviceProvisioningInteractor by Fixture {
+    DeviceProvisioningInteractor(
+        repository = deviceProvisioningRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
new file mode 100644
index 0000000..117ae8c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.sensors
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.asyncSensorManager by Kosmos.Fixture { mock<AsyncSensorManager>() }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 3aa9cc8..155c523 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -132,7 +132,7 @@
 
     private static final String CAMERA_EXTENSION_VERSION_NAME =
             "androidx.camera.extensions.impl.ExtensionVersionImpl";
-    private static final String LATEST_VERSION = "1.4.0";
+    private static final String LATEST_VERSION = "1.5.0";
     // No support for the init sequence
     private static final String NON_INIT_VERSION_PREFIX = "1.0";
     // Support advanced API and latency queries
@@ -1693,6 +1693,7 @@
         private final Size mSize;
         private final int mImageFormat;
         private final int mDataspace;
+        private final long mUsage;
 
         public OutputSurfaceImplStub(OutputSurface outputSurface) {
             mSurface = outputSurface.surface;
@@ -1700,8 +1701,10 @@
             mImageFormat = outputSurface.imageFormat;
             if (mSurface != null) {
                 mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
+                mUsage = SurfaceUtils.getSurfaceUsage(mSurface);
             } else {
                 mDataspace = -1;
+                mUsage = 0;
             }
         }
 
@@ -1724,6 +1727,11 @@
         public int getDataspace() {
             return mDataspace;
         }
+
+        @Override
+        public long getUsage() {
+            return mUsage;
+        }
     }
 
     private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements
@@ -2471,6 +2479,11 @@
             ret.size.height = imageReaderOutputConfig.getSize().getHeight();
             ret.imageFormat = imageReaderOutputConfig.getImageFormat();
             ret.capacity = imageReaderOutputConfig.getMaxImages();
+            if (EFV_SUPPORTED) {
+                ret.usage = imageReaderOutputConfig.getUsage();
+            } else {
+                ret.usage = 0;
+            }
         } else if (output instanceof MultiResolutionImageReaderOutputConfigImpl) {
             MultiResolutionImageReaderOutputConfigImpl multiResReaderConfig =
                     (MultiResolutionImageReaderOutputConfigImpl) output;
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index e013a3e..1ac69f6 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -30,6 +30,9 @@
         "junit-src/**/*.java",
         "junit-impl-src/**/*.java",
     ],
+    static_libs: [
+        "androidx.test.monitor-for-device",
+    ],
     libs: [
         "framework-minus-apex.ravenwood",
         "junit",
@@ -61,3 +64,17 @@
         "core-xml-for-host",
     ],
 }
+
+java_host_for_device {
+    name: "androidx.test.monitor-for-device",
+    libs: [
+        "androidx.test.monitor-for-host",
+    ],
+}
+
+java_device_for_host {
+    name: "androidx.test.monitor-for-host",
+    libs: [
+        "androidx.test.monitor",
+    ],
+}
diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py
new file mode 100644
index 0000000..36d398c
--- /dev/null
+++ b/ravenwood/bulk_enable.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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.
+
+"""
+Tool to bulk-enable tests that are now passing on Ravenwood.
+
+Currently only offers to include classes which are fully passing; ignores
+classes that have partial success.
+
+Typical usage:
+$ ENABLE_PROBE_IGNORED=1 atest MyTestsRavenwood
+$ cd /path/to/tests/root
+$ python bulk_enable.py /path/to/atest/output/host_log.txt
+"""
+
+import collections
+import os
+import re
+import subprocess
+import sys
+
+re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$")
+
+ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood"
+SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION)
+
+STATE_PASSED = "PASSED"
+STATE_FAILURE = "FAILURE"
+STATE_ASSUMPTION_FAILURE = "ASSUMPTION_FAILURE"
+STATE_CANDIDATE = "CANDIDATE"
+
+stats_total = collections.defaultdict(int)
+stats_class = collections.defaultdict(lambda: collections.defaultdict(int))
+stats_method = collections.defaultdict()
+
+with open(sys.argv[1]) as f:
+    for line in f.readlines():
+        result = re_result.search(line)
+        if result:
+            clazz, method, state, msg = result.groups()
+            if state == STATE_FAILURE and "actually passed under Ravenwood" in msg:
+                state = STATE_CANDIDATE
+            stats_total[state] += 1
+            stats_class[clazz][state] += 1
+            stats_method[(clazz, method)] = state
+
+# Find classes who are fully "candidates" (would be entirely green if enabled)
+num_enabled = 0
+for clazz in stats_class.keys():
+    stats = stats_class[clazz]
+    if STATE_CANDIDATE in stats and len(stats) == 1:
+        num_enabled += stats[STATE_CANDIDATE]
+        print("Enabling fully-passing class", clazz)
+        clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1]))
+        for root, dirs, files in os.walk("."):
+            for f in files:
+                if clazz_match.match(f):
+                    path = os.path.join(root, f)
+                    subprocess.run(["sed", "-i", "-E", SED_ARG, path])
+
+print("Overall stats", stats_total)
+print("Candidates actually enabled", num_enabled)
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index b3dbcde..3670459 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,17 +16,47 @@
 
 package android.platform.test.ravenwood;
 
+import android.app.Instrumentation;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.os.RuntimeInit;
+
+import org.junit.runner.Description;
+
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
 public class RavenwoodRuleImpl {
     private static final String MAIN_THREAD_NAME = "RavenwoodMain";
 
+    /**
+     * When enabled, attempt to dump all thread stacks just before we hit the
+     * overall Tradefed timeout, to aid in debugging deadlocks.
+     */
+    private static final boolean ENABLE_TIMEOUT_STACKS = false;
+    private static final int TIMEOUT_MILLIS = 9_000;
+
+    private static final ScheduledExecutorService sTimeoutExecutor =
+            Executors.newScheduledThreadPool(1);
+
+    private static ScheduledFuture<?> sPendingTimeout;
+
     public static boolean isOnRavenwood() {
         return true;
     }
 
     public static void init(RavenwoodRule rule) {
+        RuntimeInit.redirectLogStreams();
+
         android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
         android.os.Binder.init$ravenwood();
         android.os.SystemProperties.init$ravenwood(
@@ -41,9 +71,22 @@
             main.start();
             Looper.setMainLooperForTest(main.getLooper());
         }
+
+        InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY);
+
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
+                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        }
     }
 
     public static void reset(RavenwoodRule rule) {
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout.cancel(false);
+        }
+
+        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+
         if (rule.mProvideMainThread) {
             Looper.getMainLooper().quit();
             Looper.clearMainLooperForTest();
@@ -55,4 +98,26 @@
         android.os.Binder.reset$ravenwood();
         android.os.Process.reset$ravenwood();
     }
+
+    public static void logTestRunner(String label, Description description) {
+        // This message string carefully matches the exact format emitted by on-device tests, to
+        // aid developers in debugging raw text logs
+        Log.e("TestRunner", label + ": " + description.getMethodName()
+                + "(" + description.getTestClass().getName() + ")");
+    }
+
+    private static void dumpStacks() {
+        final PrintStream out = System.err;
+        out.println("-----BEGIN ALL THREAD STACKS-----");
+        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
+            out.println();
+            Thread t = stack.getKey();
+            out.println(t.toString() + " ID=" + t.getId());
+            for (StackTraceElement e : stack.getValue()) {
+                out.println("\tat " + e);
+            }
+        }
+        out.println("-----END ALL THREAD STACKS-----");
+    }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index 68b163e..8d76970 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -19,6 +19,7 @@
 import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
 import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
 import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
+import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
 
 import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.EnabledOnRavenwood;
@@ -45,6 +46,7 @@
         }
 
         if (ENABLE_PROBE_IGNORED) {
+            Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
             // Pass through to possible underlying RavenwoodRule for both environment
             // configuration and handling method-level annotations
             return base;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 952ee0e..0285b38 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -27,7 +27,9 @@
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
 
 /**
  * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when
@@ -55,6 +57,43 @@
     static final boolean ENABLE_PROBE_IGNORED = "1".equals(
             System.getenv("RAVENWOOD_RUN_DISABLED_TESTS"));
 
+    /**
+     * When using ENABLE_PROBE_IGNORED, you may still want to skip certain tests,
+     * for example because the test would crash the JVM.
+     *
+     * This regex defines the tests that should still be disabled even if ENABLE_PROBE_IGNORED
+     * is set.
+     *
+     * Before running each test class and method, we check if this pattern can be found in
+     * the full test name (either [class full name], or [class full name] + "#" + [method name]),
+     * and if so, we skip it.
+     *
+     * For example, if you want to skip an entire test class, use:
+     * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest$'
+     *
+     * For example, if you want to skip an entire test class, use:
+     * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest#testSimple$'
+     *
+     * To ignore multiple classes, use (...|...), for example:
+     * RAVENWOOD_REALLY_DISABLE='\.(ClassA|ClassB)$'
+     *
+     * Because we use a regex-find, setting "." would disable all tests.
+     */
+    private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile(
+            Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), ""));
+
+    private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
+            !REALLY_DISABLE_PATTERN.pattern().isEmpty();
+
+    static {
+        if (ENABLE_PROBE_IGNORED) {
+            System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
+            if (ENABLE_REALLY_DISABLE_PATTERN) {
+                System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern());
+            }
+        }
+    }
+
     private static final int SYSTEM_UID = 1000;
     private static final int NOBODY_UID = 9999;
     private static final int FIRST_APPLICATION_UID = 10000;
@@ -203,6 +242,21 @@
         return true;
     }
 
+    static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) {
+        if (!ENABLE_REALLY_DISABLE_PATTERN) {
+            return false;
+        }
+
+        final var fullname = description.getTestClass().getName()
+                + (description.isTest() ? "#" + description.getMethodName() : "");
+
+        if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) {
+            System.out.println("Still ignoring " + fullname);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public Statement apply(Statement base, Description description) {
         // No special treatment when running outside Ravenwood; run tests as-is
@@ -226,9 +280,14 @@
             public void evaluate() throws Throwable {
                 Assume.assumeTrue(shouldEnableOnRavenwood(description));
 
+                RavenwoodRuleImpl.logTestRunner("started", description);
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
+                    RavenwoodRuleImpl.logTestRunner("finished", description);
+                } catch (Throwable t) {
+                    RavenwoodRuleImpl.logTestRunner("failed", description);
+                    throw t;
                 } finally {
                     RavenwoodRuleImpl.reset(RavenwoodRule.this);
                 }
@@ -245,6 +304,9 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
+                Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
+
+                RavenwoodRuleImpl.logTestRunner("started", description);
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
@@ -254,6 +316,7 @@
                     Assume.assumeTrue(shouldEnableOnRavenwood(description));
                     throw t;
                 } finally {
+                    RavenwoodRuleImpl.logTestRunner("finished", description);
                     RavenwoodRuleImpl.reset(RavenwoodRule.this);
                 }
 
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index d0c2e18..7d172f2 100644
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,6 +16,8 @@
 
 package android.platform.test.ravenwood;
 
+import org.junit.runner.Description;
+
 public class RavenwoodRuleImpl {
     public static boolean isOnRavenwood() {
         return false;
@@ -28,4 +30,8 @@
     public static void reset(RavenwoodRule rule) {
         // No-op when running on a real device
     }
+
+    public static void logTestRunner(String label, Description description) {
+        // No-op when running on a real device
+    }
 }
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index eaf01a3..e49b64e 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -5,6 +5,7 @@
 com.android.internal.logging.MetricsLogger
 com.android.internal.logging.testing.FakeMetricsLogger
 com.android.internal.logging.testing.UiEventLoggerFake
+com.android.internal.os.AndroidPrintStream
 com.android.internal.os.BatteryStatsHistory
 com.android.internal.os.BatteryStatsHistory$TraceDelegate
 com.android.internal.os.BatteryStatsHistory$VarintParceler
@@ -16,6 +17,7 @@
 com.android.internal.os.PowerProfile
 com.android.internal.os.PowerStats
 com.android.internal.os.PowerStats$Descriptor
+com.android.internal.os.RuntimeInit
 com.android.internal.power.ModemPowerProfile
 
 android.util.AtomicFile
@@ -72,6 +74,7 @@
 android.os.ServiceSpecificException
 android.os.SystemClock
 android.os.SystemProperties
+android.os.TestLooperManager
 android.os.ThreadLocalWorkSource
 android.os.TimestampedValue
 android.os.Trace
@@ -141,6 +144,8 @@
 
 android.content.ContentProvider
 
+android.app.Instrumentation
+
 android.metrics.LogMaker
 
 android.view.Display$HdrCapabilities
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 57c0539..63784ba 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
 import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT;
@@ -5724,6 +5725,21 @@
     }
 
     @Override
+    public void attachAccessibilityOverlayToDisplay_enforcePermission(
+            int displayId, SurfaceControl sc) {
+        mContext.enforceCallingPermission(
+                INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission");
+        mMainHandler.sendMessage(
+                obtainMessage(
+                        AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal,
+                        this,
+                        -1,
+                        displayId,
+                        sc,
+                        null));
+    }
+
+    @Override
     public void attachAccessibilityOverlayToDisplay(
             int interactionId,
             int displayId,
@@ -5759,12 +5775,15 @@
             t.close();
             result = AccessibilityService.OVERLAY_RESULT_SUCCESS;
         }
-        // Send the result back to the service.
-        try {
-            callback.sendAttachOverlayResult(result, interactionId);
-        } catch (RemoteException re) {
-            Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
-            // the other side will time out
+
+        if (callback != null) {
+            // Send the result back to the service.
+            try {
+                callback.sendAttachOverlayResult(result, interactionId);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
+                // the other side will time out
+            }
         }
     }
 }
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index d08a97e..2a85eb6 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -21,7 +21,6 @@
     libs: ["services.core"],
     static_libs: [
         "app-compat-annotations",
-        "backup_flags_lib",
     ],
     lint: {
         baseline_filename: "lint-baseline.xml",
@@ -33,8 +32,3 @@
     package: "com.android.server.backup",
     srcs: ["flags.aconfig"],
 }
-
-java_aconfig_library {
-    name: "backup_flags_lib",
-    aconfig_declarations: "backup_flags",
-}
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 1416c88..6a63b3a 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -24,4 +24,12 @@
     description: "Enables the write buffer to pipes to be of maximum size."
     bug: "265976737"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_clear_pipe_after_restore_file"
+    namespace: "onboarding"
+    description: "Enables clearing the pipe buffer after restoring a single file to a BackupAgent."
+    bug: "320633449"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 0559708..d85dd87 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -732,6 +732,7 @@
                     BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                     monitoringExtras);
             EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+            mStatus = BackupTransport.TRANSPORT_ERROR;
             nextState = UnifiedRestoreState.FINAL;
         } finally {
             executeNextState(nextState);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 8e35b74..89896c3 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -211,7 +211,6 @@
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
-        "backup_flags_lib",
         "policy_flags_lib",
         "net_flags_lib",
         "stats_flags_lib",
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index b87184a..416b36f 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -288,22 +288,23 @@
                         CachedDeviceState.Readonly.class);
                 mBinderCallsStats.setDeviceState(deviceState);
 
-                BatteryStatsInternal batteryStatsInternal = getLocalService(
-                        BatteryStatsInternal.class);
-                mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
-                    @Override
-                    public void noteCallStats(int workSourceUid, long incrementalCallCount,
-                            Collection<BinderCallsStats.CallStat> callStats) {
-                        batteryStatsInternal.noteBinderCallStats(workSourceUid,
-                                incrementalCallCount, callStats);
-                    }
+                if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+                    BatteryStatsInternal batteryStatsInternal = getLocalService(
+                            BatteryStatsInternal.class);
+                    mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
+                        @Override
+                        public void noteCallStats(int workSourceUid, long incrementalCallCount,
+                                Collection<BinderCallsStats.CallStat> callStats) {
+                            batteryStatsInternal.noteBinderCallStats(workSourceUid,
+                                    incrementalCallCount, callStats);
+                        }
 
-                    @Override
-                    public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
-                        batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
-                    }
-                });
-
+                        @Override
+                        public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
+                            batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
+                        }
+                    });
+                }
                 // It needs to be called before mService.systemReady to make sure the observer is
                 // initialized before installing it.
                 mWorkSourceProvider.systemReady(getContext());
diff --git a/services/core/java/com/android/server/IntentResolver.java b/services/core/java/com/android/server/IntentResolver.java
index a1e6b58f..81c9ee7 100644
--- a/services/core/java/com/android/server/IntentResolver.java
+++ b/services/core/java/com/android/server/IntentResolver.java
@@ -81,7 +81,7 @@
      * Returns whether an intent matches the IntentFilter with a pre-resolved type.
      */
     public static boolean intentMatchesFilter(
-            IntentFilter filter, Intent intent, String resolvedType, boolean defaultOnly) {
+            IntentFilter filter, Intent intent, String resolvedType) {
         final boolean debug = localLOGV
                 || ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
 
@@ -97,10 +97,6 @@
         int match = filter.match(intent.getAction(), resolvedType, intent.getScheme(),
                 intent.getData(), intent.getCategories(), TAG);
 
-        if (match >= 0 && defaultOnly && !filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
-            match = IntentFilter.NO_MATCH_CATEGORY;
-        }
-
         if (match >= 0) {
             if (debug) {
                 Slog.v(TAG, "Filter matched!  match=0x" + Integer.toHexString(match));
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index c391642..f619ca3 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -27,6 +27,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -49,12 +50,12 @@
     private static final String TAG = "SensitiveContentProtect";
     private static final boolean DEBUG = false;
 
-    @VisibleForTesting
-    NotificationListener mNotificationListener;
+    @VisibleForTesting NotificationListener mNotificationListener;
     private @Nullable MediaProjectionManager mProjectionManager;
     private @Nullable WindowManagerInternal mWindowManager;
 
     final Object mSensitiveContentProtectionLock = new Object();
+
     @GuardedBy("mSensitiveContentProtectionLock")
     private boolean mProjectionActive = false;
 
@@ -63,13 +64,24 @@
                 @Override
                 public void onStart(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStart projection: " + info);
-                    onProjectionStart();
+                    Trace.beginSection(
+                            "SensitiveContentProtectionManagerService.onProjectionStart");
+                    try {
+                        onProjectionStart();
+                    } finally {
+                        Trace.endSection();
+                    }
                 }
 
                 @Override
                 public void onStop(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStop projection: " + info);
-                    onProjectionEnd();
+                    Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop");
+                    try {
+                        onProjectionEnd();
+                    } finally {
+                        Trace.endSection();
+                    }
                 }
             };
 
@@ -94,8 +106,7 @@
     }
 
     @VisibleForTesting
-    void init(MediaProjectionManager projectionManager,
-            WindowManagerInternal windowManager) {
+    void init(MediaProjectionManager projectionManager, WindowManagerInternal windowManager) {
         if (DEBUG) Log.d(TAG, "init");
 
         checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager");
@@ -109,7 +120,8 @@
         mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));
 
         try {
-            mNotificationListener.registerAsSystemService(getContext(),
+            mNotificationListener.registerAsSystemService(
+                    getContext(),
                     new ComponentName(getContext(), NotificationListener.class),
                     UserHandle.USER_ALL);
         } catch (RemoteException e) {
@@ -174,8 +186,8 @@
         }
 
         // notify windowmanager of any currently posted sensitive content notifications
-        ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications(
-                notifications, rankingMap);
+        ArraySet<PackageInfo> packageInfos =
+                getSensitivePackagesFromNotifications(notifications, rankingMap);
 
         mWindowManager.addBlockScreenCaptureForApps(packageInfos);
     }
@@ -197,8 +209,8 @@
         return sensitivePackages;
     }
 
-    private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn,
-            RankingMap rankingMap) {
+    private PackageInfo getSensitivePackageFromNotification(
+            StatusBarNotification sbn, RankingMap rankingMap) {
         if (sbn == null) {
             Log.w(TAG, "Unable to protect null notification");
             return null;
@@ -220,38 +232,55 @@
         @Override
         public void onListenerConnected() {
             super.onListenerConnected();
-            // Projection started before notification listener was connected
-            synchronized (mSensitiveContentProtectionLock) {
-                if (mProjectionActive) {
-                    updateAppsThatShouldBlockScreenCapture();
+            Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected");
+            try {
+                // Projection started before notification listener was connected
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (mProjectionActive) {
+                        updateAppsThatShouldBlockScreenCapture();
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
 
         @Override
         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
             super.onNotificationPosted(sbn, rankingMap);
-            synchronized (mSensitiveContentProtectionLock) {
-                if (!mProjectionActive) {
-                    return;
-                }
+            Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted");
+            try {
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (!mProjectionActive) {
+                        return;
+                    }
 
-                // notify windowmanager of any currently posted sensitive content notifications
-                PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
+                    // notify windowmanager of any currently posted sensitive content notifications
+                    PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
 
-                if (packageInfo != null) {
-                    mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo)));
+                    if (packageInfo != null) {
+                        mWindowManager.addBlockScreenCaptureForApps(
+                                new ArraySet(Set.of(packageInfo)));
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
 
         @Override
         public void onNotificationRankingUpdate(RankingMap rankingMap) {
             super.onNotificationRankingUpdate(rankingMap);
-            synchronized (mSensitiveContentProtectionLock) {
-                if (mProjectionActive) {
-                    updateAppsThatShouldBlockScreenCapture(rankingMap);
+            Trace.beginSection(
+                    "SensitiveContentProtectionManagerService.onNotificationRankingUpdate");
+            try {
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (mProjectionActive) {
+                        updateAppsThatShouldBlockScreenCapture(rankingMap);
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 374a17a..a5531ae 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2101,7 +2101,8 @@
         // Start watching app ops after we and the package manager are up and running.
         mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_IN_BACKGROUND, null,
                 new IAppOpsCallback.Stub() {
-                    @Override public void opChanged(int op, int uid, String packageName) {
+                    @Override public void opChanged(int op, int uid, String packageName,
+                            String persistentDeviceId) {
                         if (op == AppOpsManager.OP_RUN_IN_BACKGROUND && packageName != null) {
                             if (getAppOpsManager().checkOpNoThrow(op, uid, packageName)
                                     != AppOpsManager.MODE_ALLOWED) {
@@ -2115,7 +2116,7 @@
         mAppOpsService.startWatchingActive(cameraOp, new IAppOpsActiveCallback.Stub() {
             @Override
             public void opActiveChanged(int op, int uid, String packageName, String attributionTag,
-                    boolean active, @AttributionFlags int attributionFlags,
+                    int virtualDeviceId, boolean active, @AttributionFlags int attributionFlags,
                     int attributionChainId) {
                 cameraActiveChanged(uid, active);
             }
diff --git a/services/core/java/com/android/server/am/AppPermissionTracker.java b/services/core/java/com/android/server/am/AppPermissionTracker.java
index 18a9153..c641b35 100644
--- a/services/core/java/com/android/server/am/AppPermissionTracker.java
+++ b/services/core/java/com/android/server/am/AppPermissionTracker.java
@@ -393,7 +393,7 @@
 
     private class MyAppOpsCallback extends IAppOpsCallback.Stub {
         @Override
-        public void opChanged(int op, int uid, String packageName) {
+        public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
             mHandler.obtainMessage(MyHandler.MSG_APPOPS_CHANGED, op, uid, packageName)
                     .sendToTarget();
         }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 3487ae3..4f46ecd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -422,7 +422,9 @@
         mStats.setExternalStatsSyncLocked(mWorker);
         mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
-        mStats.startTrackingSystemServerCpuTime();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mStats.startTrackingSystemServerCpuTime();
+        }
 
         mAggregatedPowerStatsConfig = createAggregatedPowerStatsConfig();
         mPowerStatsStore = new PowerStatsStore(systemDir, mHandler, mAggregatedPowerStatsConfig);
diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java
index a20623c..5df9107 100644
--- a/services/core/java/com/android/server/am/PendingIntentController.java
+++ b/services/core/java/com/android/server/am/PendingIntentController.java
@@ -30,6 +30,7 @@
 import android.app.AppGlobals;
 import android.app.PendingIntent;
 import android.app.PendingIntentStats;
+import android.app.compat.CompatChanges;
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.os.Binder;
@@ -136,6 +137,11 @@
                         + "intent creator ("
                         + packageName
                         + ") because this option is meant for the pending intent sender");
+                if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+                        callingUid)) {
+                    throw new IllegalArgumentException("pendingIntentBackgroundActivityStartMode "
+                            + "must not be set when creating a PendingIntent");
+                }
                 opts.setPendingIntentBackgroundActivityStartMode(
                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
             }
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 10d5fd3..95e130e 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -406,6 +406,9 @@
             String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver,
             String requiredPermission, IBinder resultTo, String resultWho, int requestCode,
             int flagsMask, int flagsValues, Bundle options) {
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+
         if (intent != null) intent.setDefusable(true);
         if (options != null) options.setDefusable(true);
 
@@ -458,6 +461,12 @@
                                     + key.packageName
                                     + ") because this option is meant for the pending intent "
                                     + "creator");
+                    if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+                            callingUid)) {
+                        throw new IllegalArgumentException(
+                                "pendingIntentCreatorBackgroundActivityStartMode "
+                                + "must not be set when sending a PendingIntent");
+                    }
                     opts.setPendingIntentCreatorBackgroundActivityStartMode(
                             ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
                 }
@@ -494,9 +503,6 @@
         }
         // We don't hold the controller lock beyond this point as we will be calling into AM and WM.
 
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-
         // Only system senders can declare a broadcast to be alarm-originated.  We check
         // this here rather than in the general case handling below to fail before the other
         // invocation side effects such as allowlisting.
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7aafda5..1207616 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -162,6 +162,7 @@
         "nfc",
         "pdf_viewer",
         "pixel_audio_android",
+        "pixel_biometrics_face",
         "pixel_bluetooth",
         "pixel_connectivity_gps",
         "pixel_system_sw_video",
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index 0916967..9535db8 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -220,7 +220,7 @@
         }
 
         for (int i = 0; i < listenersCopy.size(); i++) {
-            listenersCopy.get(i).onUidModeChanged(uid, op, mode);
+            listenersCopy.get(i).onUidModeChanged(uid, op, mode, persistentDeviceId);
         }
 
         return true;
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
index f056f6b..8b4ea67 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
@@ -176,8 +176,9 @@
          * @param uid The UID whose appop mode was changed.
          * @param code The op code that was changed.
          * @param mode The new mode.
+         * @param persistentDeviceId the device whose mode was changed
          */
-        void onUidModeChanged(int uid, int code, int mode);
+        void onUidModeChanged(int uid, int code, int mode, String persistentDeviceId);
 
         /**
          * Invoked when a package's appop mode is changed.
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 2ed217a..5c95d43 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -166,6 +166,7 @@
 import com.android.server.LockGuard;
 import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemServiceManager;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.pm.PackageList;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.UserManagerInternal;
@@ -275,6 +276,10 @@
             = new AppOpsManagerInternalImpl();
     @Nullable private final DevicePolicyManagerInternal dpmi =
             LocalServices.getService(DevicePolicyManagerInternal.class);
+    @Nullable private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
+
+    /** Map of virtual device id -> persistent device id. */
+    private final SparseArray<String> mKnownDeviceIds = new SparseArray<>();
 
     private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
             ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
@@ -595,45 +600,71 @@
         final UidState uidState;
         final @NonNull String packageName;
 
-        /** attributionTag -> AttributedOp */
-        final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1);
+        /**
+         * Map to retrieve {@link AttributedOp} for a particular device and attribution tag.
+         *
+         * ArrayMap<Persistent Device Id, ArrayMap<Attribution Tag, AttributedOp>>
+         */
+        final ArrayMap<String, ArrayMap<String, AttributedOp>> mDeviceAttributedOps =
+                new ArrayMap<String, ArrayMap<String, AttributedOp>>(1);
 
         Op(UidState uidState, String packageName, int op, int uid) {
             this.op = op;
             this.uid = uid;
             this.uidState = uidState;
             this.packageName = packageName;
+            // We keep an invariant that the persistent device will always have an entry in
+            // mDeviceAttributedOps.
+            mDeviceAttributedOps.put(PERSISTENT_DEVICE_ID_DEFAULT,
+                    new ArrayMap<String, AttributedOp>());
         }
 
         void removeAttributionsWithNoTime() {
-            for (int i = mAttributions.size() - 1; i >= 0; i--) {
-                if (!mAttributions.valueAt(i).hasAnyTime()) {
-                    mAttributions.removeAt(i);
+            for (int deviceIndex = mDeviceAttributedOps.size() - 1; deviceIndex >= 0;
+                    deviceIndex--) {
+                ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.valueAt(
+                        deviceIndex);
+                for (int tagIndex = attributedOps.size() - 1; tagIndex >= 0; tagIndex--) {
+                    if (!attributedOps.valueAt(tagIndex).hasAnyTime()) {
+                        attributedOps.removeAt(tagIndex);
+                    }
+                }
+                if (!Objects.equals(PERSISTENT_DEVICE_ID_DEFAULT,
+                        mDeviceAttributedOps.keyAt(deviceIndex)) && attributedOps.isEmpty()) {
+                    mDeviceAttributedOps.removeAt(deviceIndex);
                 }
             }
         }
 
         private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
-                @Nullable String attributionTag) {
-            AttributedOp attributedOp;
+                @Nullable String attributionTag, String persistentDeviceId) {
+            ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+                    persistentDeviceId);
+            if (attributedOps == null) {
+                attributedOps = new ArrayMap<>();
+                mDeviceAttributedOps.put(persistentDeviceId, attributedOps);
+            }
+            AttributedOp attributedOp = attributedOps.get(attributionTag);
 
-            attributedOp = mAttributions.get(attributionTag);
             if (attributedOp == null) {
-                attributedOp = new AttributedOp(AppOpsService.this, attributionTag, parent);
-                mAttributions.put(attributionTag, attributedOp);
+                attributedOp = new AttributedOp(AppOpsService.this, attributionTag,
+                        persistentDeviceId, parent);
+                attributedOps.put(attributionTag, attributedOp);
             }
 
             return attributedOp;
         }
 
         @NonNull OpEntry createEntryLocked() {
-            final int numAttributions = mAttributions.size();
-
+            // TODO(b/308201969): Update this method when we introduce disk persistence of events
+            // for accesses on external devices.
+            final ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+                    PERSISTENT_DEVICE_ID_DEFAULT);
             final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries =
-                    new ArrayMap<>(numAttributions);
-            for (int i = 0; i < numAttributions; i++) {
-                attributionEntries.put(mAttributions.keyAt(i),
-                        mAttributions.valueAt(i).createAttributedOpEntryLocked());
+                    new ArrayMap<>(attributedOps.size());
+            for (int i = 0; i < attributedOps.size(); i++) {
+                attributionEntries.put(attributedOps.keyAt(i),
+                        attributedOps.valueAt(i).createAttributedOpEntryLocked());
             }
 
             return new OpEntry(
@@ -644,17 +675,15 @@
         }
 
         @NonNull OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) {
-            final int numAttributions = mAttributions.size();
-
+            // TODO(b/308201969): Update this method when we introduce disk persistence of events
+            // for accesses on external devices.
+            final ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+                    PERSISTENT_DEVICE_ID_DEFAULT);
             final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1);
-            for (int i = 0; i < numAttributions; i++) {
-                if (Objects.equals(mAttributions.keyAt(i), attributionTag)) {
-                    attributionEntries.put(mAttributions.keyAt(i),
-                            mAttributions.valueAt(i).createAttributedOpEntryLocked());
-                    break;
-                }
+            if (attributedOps.get(attributionTag) != null) {
+                attributionEntries.put(attributionTag,
+                        attributedOps.get(attributionTag).createAttributedOpEntryLocked());
             }
-
             return new OpEntry(
                     op,
                     mAppOpsCheckingService.getPackageMode(
@@ -663,13 +692,15 @@
         }
 
         boolean isRunning() {
-            final int numAttributions = mAttributions.size();
-            for (int i = 0; i < numAttributions; i++) {
-                if (mAttributions.valueAt(i).isRunning()) {
-                    return true;
+            for (int deviceIndex = 0; deviceIndex < mDeviceAttributedOps.size(); deviceIndex++) {
+                ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.valueAt(
+                        deviceIndex);
+                for (int tagIndex = 0; tagIndex < attributedOps.size(); tagIndex++) {
+                    if (attributedOps.valueAt(tagIndex).isRunning()) {
+                        return true;
+                    }
                 }
             }
-
             return false;
         }
     }
@@ -738,7 +769,15 @@
 
         @Override
         public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException {
-            mCallback.opChanged(op, uid, packageName);
+            throw new IllegalStateException(
+                    "unimplemented onOpModeChanged method called for op: " + op + " uid: " + uid
+                            + " packageName: " + packageName);
+        }
+
+        @Override
+        public void onOpModeChanged(int op, int uid, String packageName, String persistentDeviceId)
+                throws RemoteException {
+            mCallback.opChanged(op, uid, packageName, persistentDeviceId);
         }
     }
 
@@ -910,6 +949,7 @@
     public AppOpsService(File recentAccessesFile, File storageFile, Handler handler,
             Context context) {
         mContext = context;
+        mKnownDeviceIds.put(Context.DEVICE_ID_DEFAULT, PERSISTENT_DEVICE_ID_DEFAULT);
 
         for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
             int switchCode = AppOpsManager.opToSwitch(switchedCode);
@@ -927,10 +967,11 @@
         mAppOpsCheckingService.addAppOpsModeChangedListener(
                 new AppOpsCheckingServiceInterface.AppOpsModeChangedListener() {
                     @Override
-                    public void onUidModeChanged(int uid, int code, int mode) {
+                    public void onUidModeChanged(int uid, int code, int mode,
+                            String persistentDeviceId) {
                         mHandler.sendMessage(PooledLambda.obtainMessage(
                                 AppOpsService::notifyOpChangedForAllPkgsInUid, AppOpsService.this,
-                                code, uid, false));
+                                code, uid, false, persistentDeviceId));
                     }
 
                     @Override
@@ -941,8 +982,9 @@
                                 packageName, code, mode, userId));
                     }
                 });
+        // Only notify default device as other devices are unaffected by restriction changes.
         mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
-                code -> notifyWatchersOfChange(code, UID_ANY));
+                code -> notifyWatchersOnDefaultDevice(code, UID_ANY));
 
         LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
         mStorageFile = new AtomicFile(storageFile, "appops_legacy");
@@ -1043,25 +1085,28 @@
                     int numOps = ops.size();
                     for (int opNum = 0; opNum < numOps; opNum++) {
                         Op op = ops.valueAt(opNum);
+                        for (int deviceIndex = op.mDeviceAttributedOps.size() - 1; deviceIndex >= 0;
+                                deviceIndex--) {
+                            ArrayMap<String, AttributedOp> attributedOps =
+                                    op.mDeviceAttributedOps.valueAt(deviceIndex);
+                            for (int tagIndex = attributedOps.size() - 1; tagIndex >= 0;
+                                    tagIndex--) {
+                                String tag = attributedOps.keyAt(tagIndex);
+                                if (attributionTags.contains(tag)) {
+                                    // attribution still exist after upgrade
+                                    continue;
+                                }
 
-                        int numAttributions = op.mAttributions.size();
-                        for (int attributionNum = numAttributions - 1; attributionNum >= 0;
-                                attributionNum--) {
-                            String attributionTag = op.mAttributions.keyAt(attributionNum);
+                                String newAttributionTag = dstAttributionTags.get(tag);
 
-                            if (attributionTags.contains(attributionTag)) {
-                                // attribution still exist after upgrade
-                                continue;
+                                AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
+                                        newAttributionTag,
+                                        op.mDeviceAttributedOps.keyAt(deviceIndex));
+                                newAttributedOp.add(attributedOps.get(tag));
+                                attributedOps.remove(tag);
+
+                                scheduleFastWriteLocked();
                             }
-
-                            String newAttributionTag = dstAttributionTags.get(attributionTag);
-
-                            AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
-                                    newAttributionTag);
-                            newAttributedOp.add(op.mAttributions.valueAt(attributionNum));
-                            op.mAttributions.removeAt(attributionNum);
-
-                            scheduleFastWriteLocked();
                         }
                     }
                 }
@@ -1070,6 +1115,8 @@
     };
 
     public void systemReady() {
+        mVirtualDeviceManagerInternal = LocalServices.getService(
+                VirtualDeviceManagerInternal.class);
         mAppOpsCheckingService.systemReady();
         initializeUidStates();
 
@@ -1144,7 +1191,17 @@
                         final String changedPkg = changedPkgs[i];
                         // We trust packagemanager to insert matching uid and packageNames in the
                         // extras
-                        notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg);
+                        Set<String> devices;
+                        if (mVirtualDeviceManagerInternal != null) {
+                            devices = mVirtualDeviceManagerInternal.getAllPersistentDeviceIds();
+                        } else {
+                            devices = new ArraySet<>();
+                            devices.add(PERSISTENT_DEVICE_ID_DEFAULT);
+                        }
+                        for (String device: devices) {
+                            notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg,
+                                    device);
+                        }
                     }
                 }
             }
@@ -1295,17 +1352,19 @@
             final int numOps = removedOps.size();
             for (int opNum = 0; opNum < numOps; opNum++) {
                 final Op op = removedOps.valueAt(opNum);
+                for (int deviceIndex = 0; deviceIndex < op.mDeviceAttributedOps.size();
+                        deviceIndex++) {
+                    ArrayMap<String, AttributedOp> attributedOps =
+                            op.mDeviceAttributedOps.valueAt(deviceIndex);
+                    for (int tagIndex = 0; tagIndex < attributedOps.size(); tagIndex++) {
+                        AttributedOp attributedOp = attributedOps.valueAt(tagIndex);
 
-                final int numAttributions = op.mAttributions.size();
-                for (int attributionNum = 0; attributionNum < numAttributions;
-                        attributionNum++) {
-                    AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum);
-
-                    while (attributedOp.isRunning()) {
-                        attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
-                    }
-                    while (attributedOp.isPaused()) {
-                        attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
+                        while (attributedOp.isRunning()) {
+                            attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
+                        }
+                        while (attributedOp.isPaused()) {
+                            attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
+                        }
                     }
                 }
             }
@@ -1380,7 +1439,7 @@
                                     == AppOpsManager.MODE_FOREGROUND) {
                         mHandler.sendMessage(PooledLambda.obtainMessage(
                                 AppOpsService::notifyOpChangedForAllPkgsInUid,
-                                this, code, uidState.uid, true));
+                                this, code, uidState.uid, true, PERSISTENT_DEVICE_ID_DEFAULT));
                     } else if (!uidState.pkgOps.isEmpty()) {
                         final ArraySet<OnOpModeChangedListener> listenerSet =
                                 mOpModeWatchers.get(code);
@@ -1405,7 +1464,8 @@
                                         mHandler.sendMessage(PooledLambda.obtainMessage(
                                                 AppOpsService::notifyOpChanged,
                                                 this, listenerSet.valueAt(cbi), code, uidState.uid,
-                                                uidState.pkgOps.keyAt(pkgi)));
+                                                uidState.pkgOps.keyAt(pkgi),
+                                                PERSISTENT_DEVICE_ID_DEFAULT));
                                     }
                                 }
                             }
@@ -1422,14 +1482,15 @@
                     int numOps = ops.size();
                     for (int opNum = 0; opNum < numOps; opNum++) {
                         Op op = ops.valueAt(opNum);
-
-                        int numAttributions = op.mAttributions.size();
-                        for (int attributionNum = 0; attributionNum < numAttributions;
-                                attributionNum++) {
-                            AttributedOp attributedOp = op.mAttributions.valueAt(
-                                    attributionNum);
-
-                            attributedOp.onUidStateChanged(state);
+                        for (int deviceIndex = 0; deviceIndex < op.mDeviceAttributedOps.size();
+                                deviceIndex++) {
+                            ArrayMap<String, AttributedOp> attributedOps =
+                                    op.mDeviceAttributedOps.valueAt(deviceIndex);
+                            for (int tagIndex = 0; tagIndex < attributedOps.size();
+                                    tagIndex++) {
+                                AttributedOp attributedOp = attributedOps.valueAt(tagIndex);
+                                attributedOp.onUidStateChanged(state);
+                            }
                         }
                     }
                 }
@@ -1774,7 +1835,7 @@
     private void pruneOpLocked(Op op, int uid, String packageName) {
         op.removeAttributionsWithNoTime();
 
-        if (op.mAttributions.isEmpty()) {
+        if (op.mDeviceAttributedOps.isEmpty()) {
             Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false);
             if (ops != null) {
                 ops.remove(op.op);
@@ -1870,8 +1931,10 @@
                     uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code, mode)) {
                 return;
             }
+            // TODO(b/266164193): Ensure this behavior is device-aware after uid op mode for runtime
+            //  permissions is deprecated.
             if (mode != MODE_ERRORED && mode != previousMode) {
-                updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+                updateStartedOpModeForUidForDefaultDeviceLocked(code, mode == MODE_IGNORED, uid);
             }
         }
 
@@ -1884,8 +1947,10 @@
      * @param code The op that changed
      * @param uid The uid the op was changed for
      * @param onlyForeground Only notify watchers that watch for foreground changes
+     * @param persistentDeviceId device the op was changed for
      */
-    private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground) {
+    private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
+            String persistentDeviceId) {
         String[] uidPackageNames = getPackagesForUid(uid);
         ArrayMap<OnOpModeChangedListener, ArraySet<String>> callbackSpecs = null;
         synchronized (this) {
@@ -1951,17 +2016,17 @@
             final OnOpModeChangedListener callback = callbackSpecs.keyAt(i);
             final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
             if (reportedPackageNames == null) {
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsService::notifyOpChanged,
-                        this, callback, code, uid, (String) null));
+                mHandler.sendMessage(
+                        PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
+                                callback, code, uid, (String) null, persistentDeviceId));
 
             } else {
                 final int reportedPackageCount = reportedPackageNames.size();
                 for (int j = 0; j < reportedPackageCount; j++) {
                     final String reportedPackageName = reportedPackageNames.valueAt(j);
-                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsService::notifyOpChanged,
-                            this, callback, code, uid, reportedPackageName));
+                    mHandler.sendMessage(
+                            PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
+                                    callback, code, uid, reportedPackageName, persistentDeviceId));
                 }
             }
         }
@@ -1999,13 +2064,17 @@
             }
             scheduleFastWriteLocked();
             if (mode != MODE_ERRORED) {
-                updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+                // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only uid modes are device-aware,
+                // not package modes.
+                updateStartedOpModeForUidForDefaultDeviceLocked(code, mode == MODE_IGNORED, uid);
             }
         }
 
         if (repCbs != null && uid != -1) {
+            // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only uid modes are device-aware, not
+            // package modes.
             mHandler.sendMessage(PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
-                    repCbs, code, uid, packageName));
+                    repCbs, code, uid, packageName, PERSISTENT_DEVICE_ID_DEFAULT));
         }
     }
 
@@ -2161,15 +2230,15 @@
     }
 
     private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code,
-            int uid, String packageName) {
+            int uid, String packageName, String persistentDeviceId) {
         for (int i = 0; i < callbacks.size(); i++) {
             final OnOpModeChangedListener callback = callbacks.valueAt(i);
-            notifyOpChanged(callback, code, uid, packageName);
+            notifyOpChanged(callback, code, uid, packageName, persistentDeviceId);
         }
     }
 
     private void notifyOpChanged(OnOpModeChangedListener onModeChangedListener, int code,
-            int uid, String packageName) {
+            int uid, String packageName, String persistentDeviceId) {
         Objects.requireNonNull(onModeChangedListener);
 
         if (uid != UID_ANY && onModeChangedListener.getWatchingUid() >= 0
@@ -2197,7 +2266,8 @@
                         onModeChangedListener.getCallingUid())) {
                     continue;
                 }
-                onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName);
+                onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName,
+                        persistentDeviceId);
             } catch (RemoteException e) {
                 /* ignore */
             } finally {
@@ -2289,7 +2359,8 @@
             boolean changed = false;
             for (int i = mUidStates.size() - 1; i >= 0; i--) {
                 UidState uidState = mUidStates.valueAt(i);
-                // TODO(b/299330771): Check non default modes for all devices.
+                // TODO(b/266164193): Ensure this behavior is device-aware after uid op mode for
+                //  runtime permissions is deprecated.
                 SparseIntArray opModes =
                         mAppOpsCheckingService.getNonDefaultUidModes(
                                 uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT);
@@ -2301,7 +2372,6 @@
                             int previousMode = opModes.valueAt(j);
                             int newMode = isUidOpGrantedByRole(uidState.uid, code) ? MODE_ALLOWED :
                                     AppOpsManager.opToDefaultMode(code);
-                            // TODO(b/299330771): Set mode for all necessary devices.
                             mAppOpsCheckingService.setUidMode(
                                     uidState.uid,
                                     PERSISTENT_DEVICE_ID_DEFAULT,
@@ -2375,7 +2445,7 @@
                             allChanges = addChange(allChanges, curOp.op, uid, packageName,
                                     previousMode);
                             curOp.removeAttributionsWithNoTime();
-                            if (curOp.mAttributions.isEmpty()) {
+                            if (curOp.mDeviceAttributedOps.isEmpty()) {
                                 pkgOps.removeAt(j);
                             }
                         }
@@ -2399,9 +2469,18 @@
                 ArrayList<ChangeRec> reports = ent.getValue();
                 for (int i=0; i<reports.size(); i++) {
                     ChangeRec rep = reports.get(i);
-                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsService::notifyOpChanged,
-                            this, cb, rep.op, rep.uid, rep.pkg));
+                    Set<String> devices;
+                    if (mVirtualDeviceManagerInternal != null) {
+                        devices = mVirtualDeviceManagerInternal.getAllPersistentDeviceIds();
+                    } else {
+                        devices = new ArraySet<>();
+                        devices.add(PERSISTENT_DEVICE_ID_DEFAULT);
+                    }
+                    for (String device: devices) {
+                        mHandler.sendMessage(PooledLambda.obtainMessage(
+                                AppOpsService::notifyOpChanged,
+                                this, cb, rep.op, rep.uid, rep.pkg, device));
+                    }
                 }
             }
         }
@@ -2595,6 +2674,12 @@
     private int checkOperationImpl(int code, int uid, String packageName,
              @Nullable String attributionTag, int virtualDeviceId, boolean raw) {
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(virtualDeviceId)) {
+            Slog.w(TAG,
+                    "checkOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+                            + " is invalid");
+            return AppOpsManager.MODE_IGNORED;
+        }
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             return AppOpsManager.opToDefaultMode(code);
         }
@@ -2603,7 +2688,8 @@
         if (resolvedPackageName == null) {
             return AppOpsManager.MODE_IGNORED;
         }
-        return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw);
+        return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
+                virtualDeviceId, raw);
     }
 
     /**
@@ -2617,7 +2703,7 @@
      * @return The mode of the op
      */
     private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, boolean raw) {
+            @Nullable String attributionTag, int virtualDeviceId, boolean raw) {
         PackageVerificationResult pvr;
         try {
             pvr = verifyAndGetBypass(uid, packageName, null);
@@ -2630,19 +2716,19 @@
             return AppOpsManager.MODE_IGNORED;
         }
         synchronized (this) {
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId,
+                    pvr.bypass, true)) {
                 return AppOpsManager.MODE_IGNORED;
             }
             code = AppOpsManager.opToSwitch(code);
             UidState uidState = getUidStateLocked(uid, false);
-            // TODO(b/299330771): Check mode for the relevant device.
             if (uidState != null
                     && mAppOpsCheckingService.getUidMode(
-                                    uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code)
+                                    uidState.uid, getPersistentId(virtualDeviceId), code)
                             != AppOpsManager.opToDefaultMode(code)) {
                 final int rawMode =
                         mAppOpsCheckingService.getUidMode(
-                                uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code);
+                                uidState.uid, getPersistentId(virtualDeviceId), code);
                 return raw ? rawMode : uidState.evalMode(code, rawMode);
             }
             Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
@@ -2683,8 +2769,9 @@
         mAudioRestrictionManager.setZenModeAudioRestriction(
                 code, usage, uid, mode, exceptionPackages);
 
+        // Only notify default device as other devices are unaffected by restriction changes.
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+                AppOpsService::notifyWatchersOnDefaultDevice, this, code, UID_ANY));
     }
 
 
@@ -2694,11 +2781,12 @@
 
         mAudioRestrictionManager.setCameraAudioRestriction(mode);
 
+        // Only notify default device as other devices are unaffected by restriction changes.
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsService::notifyWatchersOfChange, this,
+                AppOpsService::notifyWatchersOnDefaultDevice, this,
                 AppOpsManager.OP_PLAY_AUDIO, UID_ANY));
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsService::notifyWatchersOfChange, this,
+                AppOpsService::notifyWatchersOnDefaultDevice, this,
                 AppOpsManager.OP_VIBRATE, UID_ANY));
     }
 
@@ -2761,11 +2849,18 @@
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
         final int proxiedUid = attributionSource.getNextUid();
+        final int proxyVirtualDeviceId = attributionSource.getDeviceId();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+            Slog.w(TAG, "noteProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
+                    + proxyVirtualDeviceId + " is invalid");
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
+                    proxiedPackageName);
+        }
         if (!isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))
                 || !isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))) {
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, proxiedAttributionTag,
@@ -2792,8 +2887,9 @@
                     : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
 
             final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid,
-                    resolveProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
-                    proxyFlags, !isProxyTrusted, "proxy " + message, shouldCollectMessage);
+                    resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
+                    Process.INVALID_UID, null, null, proxyFlags, !isProxyTrusted,
+                    "proxy " + message, shouldCollectMessage);
             if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) {
                 return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag,
                         proxiedPackageName);
@@ -2810,8 +2906,9 @@
         final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
                 : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
         return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
-                proxiedAttributionTag, proxyUid, resolveProxyPackageName, proxyAttributionTag,
-                proxiedFlags, shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolveProxyPackageName,
+                proxyAttributionTag, proxiedFlags, shouldCollectAsyncNotedOp, message,
+                shouldCollectMessage);
     }
 
     @Override
@@ -2838,6 +2935,13 @@
              boolean shouldCollectMessage) {
         verifyIncomingUid(uid);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(virtualDeviceId)) {
+            Slog.w(TAG,
+                    "checkOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+                            + " is invalid");
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+                    packageName);
+        }
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
             if (code == OP_BLUETOOTH_CONNECT) {
@@ -2854,15 +2958,16 @@
                     packageName);
         }
         return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
-                Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF,
-                shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+                virtualDeviceId, Process.INVALID_UID, null, null,
+                AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp, message,
+                shouldCollectMessage);
     }
 
     private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
-           @Nullable String attributionTag, int proxyUid, String proxyPackageName,
-           @Nullable String proxyAttributionTag, @OpFlags int flags,
-           boolean shouldCollectAsyncNotedOp, @Nullable String message,
-           boolean shouldCollectMessage) {
+            @Nullable String attributionTag, int virtualDeviceId, int proxyUid,
+            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
+            boolean shouldCollectAsyncNotedOp, @Nullable String message,
+            boolean shouldCollectMessage) {
         PackageVerificationResult pvr;
         try {
             pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -2891,8 +2996,8 @@
             final Ops ops = getOpsLocked(uid, packageName, attributionTag,
                     pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
             if (ops == null) {
-                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                        AppOpsManager.MODE_IGNORED);
+                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+                        virtualDeviceId, flags, AppOpsManager.MODE_IGNORED);
                 if (DEBUG) Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
                         + " package " + packageName + "flags: " +
                         AppOpsManager.flagsToString(flags));
@@ -2911,7 +3016,8 @@
                         packageName);
             }
             final Op op = getOpLocked(ops, code, uid, true);
-            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag,
+                    getPersistentId(virtualDeviceId));
             if (attributedOp.isRunning()) {
                 Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code "
                         + code + " startTime of in progress event="
@@ -2920,33 +3026,33 @@
 
             final int switchCode = AppOpsManager.opToSwitch(code);
             final UidState uidState = ops.uidState;
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId,
+                    pvr.bypass, false)) {
                 attributedOp.rejected(uidState.getState(), flags);
-                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                        AppOpsManager.MODE_IGNORED);
+                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+                        virtualDeviceId, flags, AppOpsManager.MODE_IGNORED);
                 return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
                         packageName);
             }
-            // TODO(b/299330771): Check mode for the relevant device.
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
             if (mAppOpsCheckingService.getUidMode(
-                            uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+                            uidState.uid, getPersistentId(virtualDeviceId), switchCode)
                     != AppOpsManager.opToDefaultMode(switchCode)) {
                 final int uidMode =
                         uidState.evalMode(
                                 code,
                                 mAppOpsCheckingService.getUidMode(
                                         uidState.uid,
-                                        PERSISTENT_DEVICE_ID_DEFAULT,
+                                        getPersistentId(virtualDeviceId),
                                         switchCode));
                 if (uidMode != AppOpsManager.MODE_ALLOWED) {
                     if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
                             + switchCode + " (" + code + ") uid " + uid + " package "
                             + packageName + " flags: " + AppOpsManager.flagsToString(flags));
                     attributedOp.rejected(uidState.getState(), flags);
-                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                            uidMode);
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+                            virtualDeviceId, flags, uidMode);
                     // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
                     if (code == OP_BLUETOOTH_CONNECT && uidMode == MODE_ERRORED) {
                         Slog.e(TAG, "noting OP_BLUETOOTH_CONNECT returned MODE_ERRORED as"
@@ -2969,8 +3075,8 @@
                             + switchCode + " (" + code + ") uid " + uid + " package "
                             + packageName + " flags: " + AppOpsManager.flagsToString(flags));
                     attributedOp.rejected(uidState.getState(), flags);
-                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                            mode);
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+                            virtualDeviceId, flags, mode);
                     // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
                     if (code == OP_BLUETOOTH_CONNECT && mode == MODE_ERRORED) {
                         Slog.e(TAG, "noting OP_BLUETOOTH_CONNECT returned MODE_ERRORED as"
@@ -2986,11 +3092,10 @@
                                 : "." + attributionTag) + " flags: "
                                 + AppOpsManager.flagsToString(flags));
             }
-            scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                    AppOpsManager.MODE_ALLOWED);
+            scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+                    virtualDeviceId, flags, AppOpsManager.MODE_ALLOWED);
             attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
-                    uidState.getState(),
-                    flags);
+                    uidState.getState(), flags);
 
             if (shouldCollectAsyncNotedOp) {
                 collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message,
@@ -3314,6 +3419,13 @@
             int attributionChainId) {
         verifyIncomingUid(uid);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(virtualDeviceId)) {
+            Slog.w(TAG,
+                    "startOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+                            + " is invalid");
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+                    packageName);
+        }
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
@@ -3349,9 +3461,9 @@
         }
 
         return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
-                Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault,
-                shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags,
-                attributionChainId);
+                virtualDeviceId, Process.INVALID_UID, null, null, OP_FLAG_SELF,
+                startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+                attributionFlags, attributionChainId);
     }
 
     /** @deprecated Use {@link #startProxyOperationWithState} instead. */
@@ -3390,11 +3502,18 @@
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
         final int proxiedUid = attributionSource.getNextUid();
+        final int proxyVirtualDeviceId = attributionSource.getDeviceId();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+            Slog.w(TAG, "startProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
+                    + proxyVirtualDeviceId + " is invalid");
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
+                    proxiedPackageName);
+        }
         if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
                 || !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, proxiedAttributionTag,
@@ -3435,7 +3554,8 @@
             // Test if the proxied operation will succeed before starting the proxy operation
             final SyncNotedAppOp testProxiedOp = startOperationDryRun(code,
                     proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag,
-                    resolvedProxyPackageName, proxiedFlags, startIfModeDefault);
+                    proxyVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
+                    startIfModeDefault);
 
             if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
                 return testProxiedOp;
@@ -3445,8 +3565,9 @@
                     : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
 
             final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
-                    resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
-                    proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message,
+                    resolvedProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
+                    Process.INVALID_UID, null, null, proxyFlags,
+                    startIfModeDefault, !isProxyTrusted, "proxy " + message,
                     shouldCollectMessage, proxyAttributionFlags, attributionChainId);
             if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
                 return proxyAppOp;
@@ -3454,9 +3575,9 @@
         }
 
         return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
-                proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag,
-                proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message,
-                shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
+                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolvedProxyPackageName,
+                proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp,
+                message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
     }
 
     private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
@@ -3464,11 +3585,11 @@
     }
 
     private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid,
-            @NonNull String packageName, @Nullable String attributionTag, int proxyUid,
-            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
-            boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @Nullable String message,
-            boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
-            int attributionChainId) {
+            @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
+            int proxyUid, String proxyPackageName, @Nullable String proxyAttributionTag,
+            @OpFlags int flags, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
+            @Nullable String message, boolean shouldCollectMessage,
+            @AttributionFlags int attributionFlags, int attributionChainId) {
         PackageVerificationResult pvr;
         try {
             pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -3492,8 +3613,8 @@
                     pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
             if (ops == null) {
                 scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                        flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags,
-                        attributionChainId);
+                        virtualDeviceId, flags, AppOpsManager.MODE_IGNORED, startType,
+                        attributionFlags, attributionChainId);
                 if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
                         + " package " + packageName + " flags: "
                         + AppOpsManager.flagsToString(flags));
@@ -3501,23 +3622,23 @@
                         packageName);
             }
             final Op op = getOpLocked(ops, code, uid, true);
-            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag,
+                    getPersistentId(virtualDeviceId));
             final UidState uidState = ops.uidState;
-            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
-                    false);
+            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
+                    virtualDeviceId, pvr.bypass, false);
             final int switchCode = AppOpsManager.opToSwitch(code);
-            // TODO(b/299330771): Check mode for the relevant device.
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
             if (mAppOpsCheckingService.getUidMode(
-                            uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+                            uidState.uid, getPersistentId(virtualDeviceId), switchCode)
                     != AppOpsManager.opToDefaultMode(switchCode)) {
                 final int uidMode =
                         uidState.evalMode(
                                 code,
                                 mAppOpsCheckingService.getUidMode(
                                         uidState.uid,
-                                        PERSISTENT_DEVICE_ID_DEFAULT,
+                                        getPersistentId(virtualDeviceId),
                                         switchCode));
                 if (!shouldStartForMode(uidMode, startIfModeDefault)) {
                     if (DEBUG) {
@@ -3527,7 +3648,8 @@
                     }
                     attributedOp.rejected(uidState.getState(), flags);
                     scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                            flags, uidMode, startType, attributionFlags, attributionChainId);
+                            virtualDeviceId, flags, uidMode, startType, attributionFlags,
+                            attributionChainId);
                     return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
                 }
             } else {
@@ -3547,7 +3669,8 @@
                             + packageName + " flags: " + AppOpsManager.flagsToString(flags));
                     attributedOp.rejected(uidState.getState(), flags);
                     scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                            flags, mode, startType, attributionFlags, attributionChainId);
+                            virtualDeviceId, flags, mode, startType, attributionFlags,
+                            attributionChainId);
                     return new SyncNotedAppOp(mode, code, attributionTag, packageName);
                 }
             }
@@ -3557,19 +3680,19 @@
             try {
                 if (isRestricted) {
                     attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
-                            proxyAttributionTag, uidState.getState(), flags,
+                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
                             attributionFlags, attributionChainId);
                 } else {
                     attributedOp.started(clientId, proxyUid, proxyPackageName,
-                            proxyAttributionTag, uidState.getState(), flags,
+                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
                             attributionFlags, attributionChainId);
                     startType = START_TYPE_STARTED;
                 }
             } catch (RemoteException e) {
                 throw new RuntimeException(e);
             }
-            scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                    isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
+            scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, virtualDeviceId,
+                    flags, isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
                     attributionChainId);
         }
 
@@ -3590,7 +3713,7 @@
      * the proxied app can successfully start the operation.
      */
     private SyncNotedAppOp startOperationDryRun(int code, int uid,
-            @NonNull String packageName, @Nullable String attributionTag,
+            @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
             String proxyPackageName, @OpFlags int flags,
             boolean startIfModeDefault) {
         PackageVerificationResult pvr;
@@ -3624,21 +3747,20 @@
             }
             final Op op = getOpLocked(ops, code, uid, true);
             final UidState uidState = ops.uidState;
-            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
-                    false);
+            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
+                    virtualDeviceId, pvr.bypass, false);
             final int switchCode = AppOpsManager.opToSwitch(code);
-            // TODO(b/299330771): Check mode for the relevant device.
             // If there is a non-default mode per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
             if (mAppOpsCheckingService.getUidMode(
-                            uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+                            uidState.uid, getPersistentId(virtualDeviceId), switchCode)
                     != AppOpsManager.opToDefaultMode(switchCode)) {
                 final int uidMode =
                         uidState.evalMode(
                                 code,
                                 mAppOpsCheckingService.getUidMode(
                                         uidState.uid,
-                                        PERSISTENT_DEVICE_ID_DEFAULT,
+                                        getPersistentId(virtualDeviceId),
                                         switchCode));
                 if (!shouldStartForMode(uidMode, startIfModeDefault)) {
                     if (DEBUG) {
@@ -3697,6 +3819,11 @@
             String attributionTag, int virtualDeviceId) {
         verifyIncomingUid(uid);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(virtualDeviceId)) {
+            Slog.w(TAG, "finishOperationImpl was a no-op as virtualDeviceId " + virtualDeviceId
+                    + " is invalid");
+            return;
+        }
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             return;
         }
@@ -3706,7 +3833,8 @@
             return;
         }
 
-        finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag);
+        finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag,
+                virtualDeviceId);
     }
 
     /** @deprecated Use {@link #finishProxyOperationWithState} instead. */
@@ -3731,6 +3859,7 @@
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
         final int proxiedUid = attributionSource.getNextUid();
+        final int proxyVirtualDeviceId = attributionSource.getDeviceId();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
 
@@ -3739,6 +3868,11 @@
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
+        if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+            Slog.w(TAG, "finishProxyOperationImpl was a no-op as virtualDeviceId "
+                    + proxyVirtualDeviceId + " is invalid");
+            return null;
+        }
         if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
                 || !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
             return null;
@@ -3752,7 +3886,7 @@
 
         if (!skipProxyOperation) {
             finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
-                    proxyAttributionTag);
+                    proxyAttributionTag, proxyVirtualDeviceId);
         }
 
         String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
@@ -3762,13 +3896,13 @@
         }
 
         finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
-                proxiedAttributionTag);
+                proxiedAttributionTag, proxyVirtualDeviceId);
 
         return null;
     }
 
     private void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName,
-            String attributionTag) {
+            String attributionTag, int virtualDeviceId) {
         PackageVerificationResult pvr;
         try {
             pvr = verifyAndGetBypass(uid, packageName, attributionTag);
@@ -3788,7 +3922,9 @@
                         + attributionTag + ") op=" + AppOpsManager.opToName(code));
                 return;
             }
-            final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
+            final AttributedOp attributedOp =
+                    op.mDeviceAttributedOps.getOrDefault(getPersistentId(virtualDeviceId),
+                            new ArrayMap<>()).get(attributionTag);
             if (attributedOp == null) {
                 Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "("
                         + attributionTag + ") op=" + AppOpsManager.opToName(code));
@@ -3805,8 +3941,8 @@
     }
 
     void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull
-            String packageName, @Nullable String attributionTag, boolean active, @AttributionFlags
-            int attributionFlags, int attributionChainId) {
+            String packageName, @Nullable String attributionTag, int virtualDeviceId,
+            boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
         ArraySet<ActiveCallback> dispatchedCallbacks = null;
         final int callbackListCount = mActiveWatchers.size();
         for (int i = 0; i < callbackListCount; i++) {
@@ -3827,13 +3963,14 @@
         }
         mHandler.sendMessage(PooledLambda.obtainMessage(
                 AppOpsService::notifyOpActiveChanged,
-                this, dispatchedCallbacks, code, uid, packageName, attributionTag, active,
-                attributionFlags, attributionChainId));
+                this, dispatchedCallbacks, code, uid, packageName, attributionTag,
+                virtualDeviceId, active, attributionFlags, attributionChainId));
     }
 
     private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks,
             int code, int uid, @NonNull String packageName, @Nullable String attributionTag,
-            boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
+            int virtualDeviceId, boolean active, @AttributionFlags int attributionFlags,
+            int attributionChainId) {
         // There are features watching for mode changes such as window manager
         // and location manager which are in our process. The callbacks in these
         // features may require permissions our remote caller does not have.
@@ -3847,7 +3984,7 @@
                         continue;
                     }
                     callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag,
-                            active, attributionFlags, attributionChainId);
+                            virtualDeviceId, active, attributionFlags, attributionChainId);
                 } catch (RemoteException e) {
                     /* do nothing */
                 }
@@ -3858,7 +3995,7 @@
     }
 
     void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
-            String attributionTag, @OpFlags int flags, @Mode int result,
+            String attributionTag, int virtualDeviceId, @OpFlags int flags, @Mode int result,
             @AppOpsManager.OnOpStartedListener.StartedType int startedType,
             @AttributionFlags int attributionFlags, int attributionChainId) {
         ArraySet<StartedCallback> dispatchedCallbacks = null;
@@ -3885,13 +4022,14 @@
 
         mHandler.sendMessage(PooledLambda.obtainMessage(
                 AppOpsService::notifyOpStarted,
-                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
-                result, startedType, attributionFlags, attributionChainId));
+                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, virtualDeviceId,
+                flags, result, startedType, attributionFlags, attributionChainId));
     }
 
     private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
-            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
-            @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType,
+            int code, int uid, String packageName, String attributionTag, int virtualDeviceId,
+            @OpFlags int flags, @Mode int result,
+            @AppOpsManager.OnOpStartedListener.StartedType int startedType,
             @AttributionFlags int attributionFlags, int attributionChainId) {
         final long identity = Binder.clearCallingIdentity();
         try {
@@ -3902,8 +4040,9 @@
                     if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
                         continue;
                     }
-                    callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
-                            result, startedType, attributionFlags, attributionChainId);
+                    callback.mCallback.opStarted(code, uid, packageName, attributionTag,
+                            virtualDeviceId, flags, result, startedType, attributionFlags,
+                            attributionChainId);
                 } catch (RemoteException e) {
                     /* do nothing */
                 }
@@ -3914,7 +4053,7 @@
     }
 
     private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName,
-            String attributionTag, @OpFlags int flags, @Mode int result) {
+            String attributionTag, int virtualDeviceId, @OpFlags int flags, @Mode int result) {
         ArraySet<NotedCallback> dispatchedCallbacks = null;
         final int callbackListCount = mNotedWatchers.size();
         for (int i = 0; i < callbackListCount; i++) {
@@ -3935,13 +4074,13 @@
         }
         mHandler.sendMessage(PooledLambda.obtainMessage(
                 AppOpsService::notifyOpChecked,
-                this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags,
-                result));
+                this, dispatchedCallbacks, code, uid, packageName, attributionTag,
+                virtualDeviceId, flags, result));
     }
 
     private void notifyOpChecked(ArraySet<NotedCallback> callbacks,
-            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
-            @Mode int result) {
+            int code, int uid, String packageName, String attributionTag, int virtualDeviceId,
+            @OpFlags int flags, @Mode int result) {
         // There are features watching for checks in our process. The callbacks in
         // these features may require permissions our remote caller does not have.
         final long identity = Binder.clearCallingIdentity();
@@ -3953,8 +4092,8 @@
                     if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
                         continue;
                     }
-                    callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags,
-                            result);
+                    callback.mCallback.opNoted(code, uid, packageName, attributionTag,
+                            virtualDeviceId, flags, result);
                 } catch (RemoteException e) {
                     /* do nothing */
                 }
@@ -4028,6 +4167,22 @@
                 watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
     }
 
+    private boolean isValidVirtualDeviceId(int virtualDeviceId) {
+        if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+            return true;
+        }
+        if (mVirtualDeviceManagerInternal == null) {
+            return true;
+        }
+        if (mVirtualDeviceManagerInternal.isValidVirtualDeviceId(virtualDeviceId)) {
+            mKnownDeviceIds.put(virtualDeviceId,
+                    mVirtualDeviceManagerInternal.getPersistentIdForDevice(virtualDeviceId));
+            return true;
+        }
+
+        return false;
+    }
+
     private void verifyIncomingOp(int op) {
         if (op >= 0 && op < AppOpsManager._NUM_OP) {
             // Enforce manage appops permission if it's a restricted read op.
@@ -4488,7 +4643,12 @@
     }
 
     private boolean isOpRestrictedLocked(int uid, int code, String packageName,
-            String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
+            String attributionTag, int virtualDeviceId, @Nullable RestrictionBypass appBypass,
+            boolean isCheckOp) {
+        // Restrictions only apply to the default device.
+        if (virtualDeviceId != Context.DEVICE_ID_DEFAULT) {
+            return false;
+        }
         int restrictionSetCount = mOpGlobalRestrictions.size();
 
         for (int i = 0; i < restrictionSetCount; i++) {
@@ -4662,7 +4822,10 @@
     private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent,
             @Nullable String attribution)
             throws NumberFormatException, IOException, XmlPullParserException {
-        final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution);
+        // TODO(b/308201969): Update this method when we introduce disk persistence of events
+        // for accesses on external devices.
+        final AttributedOp attributedOp =
+                parent.getOrCreateAttribution(parent, attribution, PERSISTENT_DEVICE_ID_DEFAULT);
 
         final long key = parser.getAttributeLong(null, "n");
         final int uidState = extractUidStateFromKey(key);
@@ -5369,18 +5532,23 @@
     private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag,
             @HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now,
             @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) {
-        final int numAttributions = op.mAttributions.size();
+        // TODO(b/299330771): Dump data for all devices.
+        ArrayMap<String, AttributedOp> defaultDeviceAttributedOps = op.mDeviceAttributedOps.get(
+                PERSISTENT_DEVICE_ID_DEFAULT);
+
+        final int numAttributions = defaultDeviceAttributedOps.size();
         for (int i = 0; i < numAttributions; i++) {
             if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals(
-                    op.mAttributions.keyAt(i), filterAttributionTag)) {
+                    defaultDeviceAttributedOps.keyAt(i), filterAttributionTag)) {
                 continue;
             }
 
-            pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n");
-            dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date,
-                    prefix + "  ");
+            pw.print(prefix + defaultDeviceAttributedOps.keyAt(i) + "=[\n");
+            dumpStatesLocked(pw, nowElapsed, op, defaultDeviceAttributedOps.keyAt(i), now, sdf,
+                    date, prefix + "  ");
             pw.print(prefix + "]\n");
         }
+
     }
 
     private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op,
@@ -5462,8 +5630,11 @@
                 pw.println();
             }
         }
+        // TODO(b/299330771): Dump running starts for all devices.
+        final AttributedOp attributedOp =
+                op.mDeviceAttributedOps.getOrDefault(PERSISTENT_DEVICE_ID_DEFAULT,
+                        new ArrayMap<>()).get(attributionTag);
 
-        final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
         if (attributedOp.isRunning()) {
             long earliestElapsedTime = Long.MAX_VALUE;
             long maxNumStarts = 0;
@@ -5828,7 +5999,7 @@
             }
             for (int i=0; i<mUidStates.size(); i++) {
                 UidState uidState = mUidStates.valueAt(i);
-                // TODO(b/299330771): Get modes for all devices.
+                // TODO(b/299330771): Dump modes for all devices.
                 final SparseIntArray opModes =
                         mAppOpsCheckingService.getNonDefaultUidModes(
                                 uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT);
@@ -6043,11 +6214,13 @@
 
             if (restrictionState.setRestriction(code, restricted, excludedPackageTags,
                     userHandle)) {
+                // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only the default device is
+                // affected by restrictions.
                 mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+                        AppOpsService::notifyWatchersOnDefaultDevice, this, code, UID_ANY));
                 mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsService::updateStartedOpModeForUser, this, code, restricted,
-                        userHandle));
+                        AppOpsService::updateStartedOpModeForUserForDefaultDevice, this, code,
+                        restricted, userHandle));
             }
 
             if (restrictionState.isDefault()) {
@@ -6057,7 +6230,8 @@
         }
     }
 
-    private void updateStartedOpModeForUser(int code, boolean restricted, int userId) {
+    private void updateStartedOpModeForUserForDefaultDevice(int code, boolean restricted,
+            int userId) {
         synchronized (AppOpsService.this) {
             int numUids = mUidStates.size();
             for (int uidNum = 0; uidNum < numUids; uidNum++) {
@@ -6065,12 +6239,13 @@
                 if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) {
                     continue;
                 }
-                updateStartedOpModeForUidLocked(code, restricted, uid);
+                updateStartedOpModeForUidForDefaultDeviceLocked(code, restricted, uid);
             }
         }
     }
 
-    private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) {
+    private void updateStartedOpModeForUidForDefaultDeviceLocked(int code, boolean restricted,
+            int uid) {
         UidState uidState = mUidStates.get(uid);
         if (uidState == null) {
             return;
@@ -6089,9 +6264,11 @@
             if (mode != MODE_ALLOWED && mode != MODE_FOREGROUND) {
                 continue;
             }
-            int numAttrTags = op.mAttributions.size();
-            for (int attrNum = 0; attrNum < numAttrTags; attrNum++) {
-                AttributedOp attrOp = op.mAttributions.valueAt(attrNum);
+            ArrayMap<String, AttributedOp> defaultDeviceAttributedOps = op.mDeviceAttributedOps.get(
+                    PERSISTENT_DEVICE_ID_DEFAULT);
+            for (int tagIndex = 0; tagIndex < defaultDeviceAttributedOps.size();
+                    tagIndex++) {
+                AttributedOp attrOp = defaultDeviceAttributedOps.valueAt(tagIndex);
                 if (restricted && attrOp.isRunning()) {
                     attrOp.pause();
                 } else if (attrOp.isPaused()) {
@@ -6101,7 +6278,7 @@
         }
     }
 
-    private void notifyWatchersOfChange(int code, int uid) {
+    private void notifyWatchersOnDefaultDevice(int code, int uid) {
         final ArraySet<OnOpModeChangedListener> modeChangedListenerSet;
         synchronized (this) {
             modeChangedListenerSet = mOpModeWatchers.get(code);
@@ -6109,8 +6286,7 @@
                 return;
             }
         }
-
-        notifyOpChanged(modeChangedListenerSet,  code, uid, null);
+        notifyOpChanged(modeChangedListenerSet,  code, uid, null, PERSISTENT_DEVICE_ID_DEFAULT);
     }
 
     @Override
@@ -6582,6 +6758,25 @@
         return packageNames;
     }
 
+    @NonNull private String getPersistentId(int virtualDeviceId) {
+        if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+            return PERSISTENT_DEVICE_ID_DEFAULT;
+        }
+        if (mVirtualDeviceManagerInternal == null) {
+            return PERSISTENT_DEVICE_ID_DEFAULT;
+        }
+        String persistentId =
+                mVirtualDeviceManagerInternal.getPersistentIdForDevice(virtualDeviceId);
+        if (persistentId == null) {
+            persistentId = mKnownDeviceIds.get(virtualDeviceId);
+        }
+        if (persistentId != null) {
+            return persistentId;
+        }
+        throw new IllegalStateException(
+                "Requested persistentId for invalid virtualDeviceId: " + virtualDeviceId);
+    }
+
     private final class ClientUserRestrictionState implements DeathRecipient {
         private final IBinder token;
 
@@ -6713,12 +6908,14 @@
                 }
 
                 if (restrictionState.setRestriction(code, restricted)) {
+                    // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only the default device is
+                    // affected by restrictions.
                     mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsService::notifyWatchersOfChange, AppOpsService.this, code,
-                            UID_ANY));
+                            AppOpsService::notifyWatchersOnDefaultDevice, AppOpsService.this,
+                            code, UID_ANY));
                     mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsService::updateStartedOpModeForUser, AppOpsService.this,
-                            code, restricted, UserHandle.USER_ALL));
+                            AppOpsService::updateStartedOpModeForUserForDefaultDevice,
+                            AppOpsService.this, code, restricted, UserHandle.USER_ALL));
                 }
 
                 if (restrictionState.isDefault()) {
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 0ded75a..94baf88 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -42,6 +42,7 @@
 final class AttributedOp {
     private final @NonNull AppOpsService mAppOpsService;
     public final @Nullable String tag;
+    public final @NonNull String persistentDeviceId;
     public final @NonNull AppOpsService.Op parent;
 
     /**
@@ -81,9 +82,10 @@
     @Nullable ArrayMap<IBinder, InProgressStartOpEvent> mPausedInProgressEvents;
 
     AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag,
-            @NonNull AppOpsService.Op parent) {
+            @NonNull String persistentDeviceId, @NonNull AppOpsService.Op parent) {
         mAppOpsService = appOpsService;
         this.tag = tag;
+        this.persistentDeviceId = persistentDeviceId;
         this.parent = parent;
     }
 
@@ -196,23 +198,26 @@
      */
     public void started(@NonNull IBinder clientId, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
-            @AppOpsManager.AttributionFlags
-                    int attributionFlags, int attributionChainId) throws RemoteException {
+            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
+            int attributionChainId) throws RemoteException {
         startedOrPaused(clientId, proxyUid, proxyPackageName,
-                proxyAttributionTag, uidState, flags, /* triggeredByUidStateChange */ false,
-                /* isStarted */ true, attributionFlags, attributionChainId);
+                proxyAttributionTag, proxyVirtualDeviceId, uidState, flags,
+                /* triggeredByUidStateChange */ false, /* isStarted */ true, attributionFlags,
+                attributionChainId);
     }
 
     @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
     private void startedOrPaused(@NonNull IBinder clientId, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
-            boolean triggeredByUidStateChange, boolean isStarted, @AppOpsManager.AttributionFlags
-            int attributionFlags, int attributionChainId) throws RemoteException {
+            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags, boolean triggeredByUidStateChange,
+            boolean isStarted, @AppOpsManager.AttributionFlags int attributionFlags,
+            int attributionChainId) throws RemoteException {
         if (!triggeredByUidStateChange && !parent.isRunning() && isStarted) {
             mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
-                    parent.packageName, tag, true, attributionFlags, attributionChainId);
+                    parent.packageName, tag, proxyVirtualDeviceId, true, attributionFlags,
+                    attributionChainId);
         }
 
         if (isStarted && mInProgressEvents == null) {
@@ -227,7 +232,7 @@
         InProgressStartOpEvent event = events.get(clientId);
         if (event == null) {
             event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime,
-                    SystemClock.elapsedRealtime(), clientId, tag,
+                    SystemClock.elapsedRealtime(), clientId, tag, proxyVirtualDeviceId,
                     PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId),
                     proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags,
                     attributionFlags, attributionChainId);
@@ -320,8 +325,8 @@
                     // TODO ntmyren: Also callback for single attribution tag activity changes
                     if (!triggeredByUidStateChange && !parent.isRunning()) {
                         mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op,
-                                parent.uid, parent.packageName, tag, false,
-                                event.getAttributionFlags(), event.getAttributionChainId());
+                                parent.uid, parent.packageName, tag, event.getVirtualDeviceId(),
+                                false, event.getAttributionFlags(), event.getAttributionChainId());
                     }
                 }
             }
@@ -362,11 +367,13 @@
      */
     public void createPaused(@NonNull IBinder clientId, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
-            @AppOpsManager.AttributionFlags
-                    int attributionFlags, int attributionChainId) throws RemoteException {
+            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags,
+            @AppOpsManager.AttributionFlags int attributionFlags,
+            int attributionChainId) throws RemoteException {
         startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag,
-                uidState, flags, false, false, attributionFlags, attributionChainId);
+                proxyVirtualDeviceId, uidState, flags, false, false,
+                attributionFlags, attributionChainId);
     }
 
     /**
@@ -387,7 +394,7 @@
             finishOrPause(event.getClientId(), false, true);
 
             mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
-                    parent.packageName, tag, false,
+                    parent.packageName, tag, event.getVirtualDeviceId(), false,
                     event.getAttributionFlags(), event.getAttributionChainId());
         }
         mInProgressEvents = null;
@@ -419,14 +426,15 @@
                     event.getAttributionFlags(), event.getAttributionChainId());
             if (shouldSendActive) {
                 mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
-                        parent.packageName, tag, true, event.getAttributionFlags(),
-                        event.getAttributionChainId());
+                        parent.packageName, tag, event.getVirtualDeviceId(), true,
+                        event.getAttributionFlags(), event.getAttributionChainId());
             }
             // Note: this always sends MODE_ALLOWED, even if the mode is FOREGROUND
             // TODO ntmyren: figure out how to get the real mode.
             mAppOpsService.scheduleOpStartedIfNeededLocked(parent.op, parent.uid,
-                    parent.packageName, tag, event.getFlags(), MODE_ALLOWED, START_TYPE_RESUMED,
-                    event.getAttributionFlags(), event.getAttributionChainId());
+                    parent.packageName, tag, event.getVirtualDeviceId(), event.getFlags(),
+                    MODE_ALLOWED, START_TYPE_RESUMED, event.getAttributionFlags(),
+                    event.getAttributionChainId());
         }
         mPausedInProgressEvents = null;
     }
@@ -488,13 +496,15 @@
                     // previously removed unfinished start counts back
                     if (proxy != null) {
                         startedOrPaused(event.getClientId(), proxy.getUid(),
-                                proxy.getPackageName(), proxy.getAttributionTag(), newState,
-                                event.getFlags(), true, isRunning,
+                                proxy.getPackageName(), proxy.getAttributionTag(),
+                                event.getVirtualDeviceId(), newState, event.getFlags(),
+                                true, isRunning,
                                 event.getAttributionFlags(), event.getAttributionChainId());
                     } else {
                         startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null,
-                                newState, event.getFlags(), true, isRunning,
-                                event.getAttributionFlags(), event.getAttributionChainId());
+                                event.getVirtualDeviceId(), newState, event.getFlags(), true,
+                                isRunning, event.getAttributionFlags(),
+                                event.getAttributionChainId());
                     }
 
                     events = isRunning ? mInProgressEvents : mPausedInProgressEvents;
@@ -508,7 +518,7 @@
                                 "Cannot switch to new uidState " + newState);
                     }
                     mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op,
-                            parent.uid, parent.packageName, tag, false,
+                            parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), false,
                             eventAttributionFlags, eventAttributionChainId);
                 }
             }
@@ -644,6 +654,9 @@
         /** Id of the client that started the event */
         private @NonNull IBinder mClientId;
 
+        /** virtual device id */
+        private int mVirtualDeviceId;
+
         /** The attribution tag for this operation */
         private @Nullable String mAttributionTag;
 
@@ -685,7 +698,7 @@
          * @throws RemoteException If the client is dying
          */
         InProgressStartOpEvent(long startTime, long startElapsedTime,
-                @NonNull IBinder clientId, @Nullable String attributionTag,
+                @NonNull IBinder clientId, int virtualDeviceId, @Nullable String attributionTag,
                 @NonNull Runnable onDeath, @AppOpsManager.UidState int uidState,
                 @Nullable AppOpsManager.OpEventProxyInfo proxy, @AppOpsManager.OpFlags int flags,
                 @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId)
@@ -693,6 +706,7 @@
             mStartTime = startTime;
             mStartElapsedTime = startElapsedTime;
             mClientId = clientId;
+            mVirtualDeviceId = virtualDeviceId;
             mAttributionTag = attributionTag;
             mOnDeath = onDeath;
             mUidState = uidState;
@@ -737,7 +751,7 @@
          * @throws RemoteException If the client is dying
          */
         public void reinit(long startTime, long startElapsedTime, @NonNull IBinder clientId,
-                @Nullable String attributionTag, @NonNull Runnable onDeath,
+                @Nullable String attributionTag, int virtualDeviceId, @NonNull Runnable onDeath,
                 @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
                 @Nullable AppOpsManager.OpEventProxyInfo proxy,
                 @AppOpsManager.AttributionFlags int attributionFlags,
@@ -749,6 +763,7 @@
             mClientId = clientId;
             mAttributionTag = attributionTag;
             mOnDeath = onDeath;
+            mVirtualDeviceId = virtualDeviceId;
             mUidState = uidState;
             mFlags = flags;
 
@@ -802,6 +817,11 @@
             return mAttributionChainId;
         }
 
+        /** @return virtual device id for the access */
+        public int getVirtualDeviceId() {
+            return mVirtualDeviceId;
+        }
+
         public void setStartTime(long startTime) {
             mStartTime = startTime;
         }
@@ -824,11 +844,11 @@
         }
 
         InProgressStartOpEvent acquire(long startTime, long elapsedTime, @NonNull IBinder clientId,
-                @Nullable String attributionTag, @NonNull Runnable onDeath, int proxyUid,
-                @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-                @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
-                @AppOpsManager.AttributionFlags
-                        int attributionFlags, int attributionChainId) throws RemoteException {
+                @Nullable String attributionTag, int virtualDeviceId,  @NonNull Runnable onDeath,
+                int proxyUid, @Nullable String proxyPackageName,
+                @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
+                @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags
+                int attributionFlags, int attributionChainId) throws RemoteException {
 
             InProgressStartOpEvent recycled = acquire();
 
@@ -839,14 +859,15 @@
             }
 
             if (recycled != null) {
-                recycled.reinit(startTime, elapsedTime, clientId, attributionTag, onDeath,
-                        uidState, flags, proxyInfo, attributionFlags, attributionChainId,
+                recycled.reinit(startTime, elapsedTime, clientId, attributionTag, virtualDeviceId,
+                        onDeath, uidState, flags, proxyInfo, attributionFlags, attributionChainId,
                         mOpEventProxyInfoPool);
                 return recycled;
             }
 
-            return new InProgressStartOpEvent(startTime, elapsedTime, clientId, attributionTag,
-                    onDeath, uidState, proxyInfo, flags, attributionFlags, attributionChainId);
+            return new InProgressStartOpEvent(startTime, elapsedTime, clientId, virtualDeviceId,
+                    attributionTag, onDeath, uidState, proxyInfo, flags, attributionFlags,
+                    attributionChainId);
         }
     }
 
diff --git a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
index 1d1a9e7..f5f34c1 100644
--- a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
+++ b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
@@ -16,8 +16,11 @@
 
 package com.android.server.appop;
 
+import android.companion.virtual.VirtualDeviceManager;
 import android.os.RemoteException;
 
+import java.util.Objects;
+
 /**
  * Listener for mode changes, encapsulates methods that should be triggered in the event of a mode
  * change.
@@ -95,6 +98,20 @@
             throws RemoteException;
 
     /**
+     * Method that should be triggered when the app-op's mode is changed.
+     * @param op app-op whose mode-change is being listened to.
+     * @param uid user-is associated with the app-op.
+     * @param packageName package name associated with the app-op.
+     * @param persistentDeviceId device associated with the app-op.
+     */
+    public void onOpModeChanged(int op, int uid, String packageName, String persistentDeviceId)
+            throws RemoteException {
+        if (Objects.equals(persistentDeviceId, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+            onOpModeChanged(op, uid, packageName);
+        }
+    }
+
+    /**
      * Return human readable string representing the listener.
      */
     public abstract String toString();
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index aa6a0f1..fbd32a6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -112,10 +112,8 @@
             getLogger().logOnError(getContext(), getOperationContext(),
                     errorCode, vendorCode, getTargetUserId());
             try {
-                if (getListener() != null) {
-                    mShouldSendErrorToClient = false;
-                    getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
-                }
+                mShouldSendErrorToClient = false;
+                getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to invoke sendError", e);
             }
@@ -147,9 +145,7 @@
 
         final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
         try {
-            if (getListener() != null) {
-                getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
-            }
+            getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to invoke sendError", e);
         }
@@ -181,7 +177,7 @@
         }
 
         try {
-            if (getListener() != null && shouldSend) {
+            if (shouldSend) {
                 getListener().onAcquired(getSensorId(), acquiredInfo, vendorCode);
             }
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index f9568ea..506b456 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -265,17 +265,13 @@
                 Slog.d(TAG, "Skipping addAuthToken");
             }
             try {
-                if (listener != null) {
-                    if (!mIsRestricted) {
-                        listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
-                                getTargetUserId(), mIsStrongBiometric);
-                    } else {
-                        listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
-                                byteToken,
-                                getTargetUserId(), mIsStrongBiometric);
-                    }
+                if (!mIsRestricted) {
+                    listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
+                            getTargetUserId(), mIsStrongBiometric);
                 } else {
-                    Slog.e(TAG, "Received successful auth, but client was not listening");
+                    listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
+                            byteToken,
+                            getTargetUserId(), mIsStrongBiometric);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Unable to notify listener", e);
@@ -301,11 +297,7 @@
                 }
 
                 try {
-                    if (listener != null) {
-                        listener.onAuthenticationFailed(getSensorId());
-                    } else {
-                        Slog.e(TAG, "Received failed auth, but client was not listening");
-                    }
+                    listener.onAuthenticationFailed(getSensorId());
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Unable to notify listener", e);
                     mCallback.onClientFinished(this, false);
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 0216e49..a408852 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -55,7 +56,7 @@
 
     @Nullable private IBinder mToken;
     private long mRequestId;
-    @Nullable private ClientMonitorCallbackConverter mListener;
+    @NonNull private ClientMonitorCallbackConverter mListener;
     // Currently only used for authentication client. The cookie generated by BiometricService
     // is never 0.
     private final int mCookie;
@@ -95,7 +96,8 @@
         mContext = context;
         mToken = token;
         mRequestId = -1;
-        mListener = listener;
+        mListener = listener == null ? new ClientMonitorCallbackConverter(
+                new IBiometricSensorReceiver.Default()) : listener;
         mTargetUserId = userId;
         mOwner = owner;
         mCookie = cookie;
@@ -199,7 +201,7 @@
         }
         mToken = null;
         if (clearListener) {
-            mListener = null;
+            mListener = new ClientMonitorCallbackConverter(new IBiometricSensorReceiver.Default());
         }
     }
 
@@ -233,8 +235,8 @@
         return mOwner;
     }
 
-    @Nullable
-    public final ClientMonitorCallbackConverter getListener() {
+    @NonNull
+    protected ClientMonitorCallbackConverter getListener() {
         return mListener;
     }
 
@@ -312,9 +314,7 @@
         final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
         try {
             ClientMonitorCallbackConverter listener = getListener();
-            if (listener != null) {
-                listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
-            }
+            listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to invoke sendError", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 2c4d30b..8e7004d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -82,9 +82,7 @@
 
         final ClientMonitorCallbackConverter listener = getListener();
         try {
-            if (listener != null) {
-                listener.onEnrollResult(identifier, remaining);
-            }
+            listener.onEnrollResult(identifier, remaining);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
index 45ffa23..d2ef278 100644
--- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
@@ -74,13 +74,9 @@
         if (identifier == null) {
             Slog.e(TAG, "identifier was null, skipping onRemove()");
             try {
-                if (getListener() != null) {
-                    getListener().onError(getSensorId(), getCookie(),
-                            BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
-                            0 /* vendorCode */);
-                } else {
-                    Slog.e(TAG, "Error, listener was null, not sending onError callback");
-                }
+                getListener().onError(getSensorId(), getCookie(),
+                        BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
+                        0 /* vendorCode */);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to send error to client for onRemoved", e);
             }
@@ -93,9 +89,7 @@
                 identifier.getBiometricId());
 
         try {
-            if (getListener() != null) {
-                getListener().onRemoved(identifier, remaining);
-            }
+            getListener().onRemoved(identifier, remaining);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to notify Removed:", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index f35de93..415d294 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -337,7 +337,7 @@
         onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
 
         final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
-        if (shouldSend && getListener() != null) {
+        if (shouldSend) {
             try {
                 getListener().onAuthenticationFrame(frame);
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index f5c4529..5f370f2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -152,7 +152,7 @@
         onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
 
         final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
-        if (shouldSend && getListener() != null) {
+        if (shouldSend) {
             try {
                 getListener().onEnrollmentFrame(frame);
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
index e404bd2..cf45eb8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
@@ -58,11 +58,6 @@
     void onChallengeGenerated(int sensorId, int userId, long challenge) {
         try {
             final ClientMonitorCallbackConverter listener = getListener();
-            if (listener == null) {
-                Slog.e(TAG, "Listener is null in onChallengeGenerated");
-                mCallback.onClientFinished(this, false /* success */);
-                return;
-            }
             listener.onChallengeGenerated(sensorId, userId, challenge);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
index 9812536..47aaeec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
@@ -61,9 +61,7 @@
     @Override
     public void unableToStart() {
         try {
-            if (getListener() != null) {
-                getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
-            }
+            getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send error", e);
         }
@@ -85,9 +83,7 @@
             featureState[0] = result.value;
             mValue = result.value;
 
-            if (getListener() != null) {
-                getListener().onFeatureGet(result.status == Status.OK, features, featureState);
-            }
+            getListener().onFeatureGet(result.status == Status.OK, features, featureState);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to getFeature", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 6912961..e0fd44b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -450,9 +450,7 @@
                         pc.major);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerDown(getSensorId());
-            }
+            getListener().onUdfpsPointerDown(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
@@ -471,9 +469,7 @@
                 session.getSession().onPointerUp(pc.pointerId);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerUp(getSensorId());
-            }
+            getListener().onUdfpsPointerUp(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index a7fb774..cb220b9e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -175,11 +175,7 @@
         vibrateSuccess();
 
         try {
-            if (getListener() != null) {
-                getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
-            } else {
-                Slog.e(TAG, "Listener is null!");
-            }
+            getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when sending onDetected", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 3fb9223..225bd59 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -305,9 +305,7 @@
                         pc.major);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerDown(getSensorId());
-            }
+            getListener().onUdfpsPointerDown(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send pointer down", e);
         }
@@ -325,9 +323,7 @@
                 session.getSession().onPointerUp(pc.pointerId);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerUp(getSensorId());
-            }
+            getListener().onUdfpsPointerUp(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send pointer up", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
index ce693ff..d2f36ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
@@ -59,11 +59,6 @@
     void onChallengeGenerated(int sensorId, int userId, long challenge) {
         try {
             final ClientMonitorCallbackConverter listener = getListener();
-            if (listener == null) {
-                Slog.e(TAG, "Listener is null in onChallengeGenerated");
-                mCallback.onClientFinished(this, false /* success */);
-                return;
-            }
             listener.onChallengeGenerated(sensorId, userId, challenge);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 9232e11..f857946 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -28,6 +28,7 @@
 import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Handler;
 import android.os.IBinder;
@@ -367,7 +368,8 @@
             final IBinder token = client.getToken();
             final long operationId = authClient.getOperationId();
             final int cookie = client.getCookie();
-            final ClientMonitorCallbackConverter listener = client.getListener();
+            final ClientMonitorCallbackConverter listener = new ClientMonitorCallbackConverter(
+                    new IFingerprintServiceReceiver.Default());
             final boolean restricted = authClient.isRestricted();
             final int statsClient = client.getLogger().getStatsClient();
             final boolean isKeyguard = authClient.isKeyguard();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 7a329e9..60c532c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -279,12 +279,10 @@
         mALSProbeCallback.getProbe().enable();
         UdfpsHelper.onFingerDown(getFreshDaemon(), (int) pc.x, (int) pc.y, pc.minor, pc.major);
 
-        if (getListener() != null) {
-            try {
-                getListener().onUdfpsPointerDown(getSensorId());
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception", e);
-            }
+        try {
+            getListener().onUdfpsPointerDown(getSensorId());
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception", e);
         }
     }
 
@@ -295,12 +293,10 @@
         mALSProbeCallback.getProbe().disable();
         UdfpsHelper.onFingerUp(getFreshDaemon());
 
-        if (getListener() != null) {
-            try {
-                getListener().onUdfpsPointerUp(getSensorId());
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception", e);
-            }
+        try {
+            getListener().onUdfpsPointerUp(getSensorId());
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception", e);
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index 6e029d2..50e48fe 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -153,12 +153,10 @@
         final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
         pm.incrementAuthForUser(getTargetUserId(), authenticated);
 
-        if (getListener() != null) {
-            try {
-                getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when sending onDetected", e);
-            }
+        try {
+            getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception when sending onDetected", e);
         }
     }
 
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index a796544..458fd82 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -1292,7 +1292,7 @@
             return;
         }
         if (DEBUG) Slog.v(TAG, "Setting NFC reader mode. enablePolling: " + enablePolling);
-        nfcAdapter.setReaderMode(enablePolling);
+        nfcAdapter.setReaderModePollingEnabled(enablePolling);
     }
 
     private static int[] toArray(Collection<Integer> c) {
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index df179a9..5b23364c 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -138,6 +138,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
@@ -3870,8 +3871,12 @@
             final SyncStorageEngine.EndPoint info = syncOperation.target;
 
             if (activeSyncContext.mIsLinkedToDeath) {
-                activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
-                activeSyncContext.mIsLinkedToDeath = false;
+                try {
+                    activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
+                    activeSyncContext.mIsLinkedToDeath = false;
+                } catch (NoSuchElementException e) {
+                    Slog.wtf(TAG, "Failed to unlink active sync adapter to death", e);
+                }
             }
             final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime;
             String historyMessage;
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index 0f40ca0..1715254 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -16,8 +16,6 @@
 
 package com.android.server.graphics.fonts;
 
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-
 import static com.android.server.graphics.fonts.FontManagerService.SystemFontException;
 
 import android.annotation.NonNull;
@@ -580,11 +578,11 @@
                 return null;
             }
             resolvedFonts.add(new FontConfig.Font(info.mFile, null, info.getPostScriptName(),
-                    font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(), null));
+                    font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(),
+                    null /* family name */, FontConfig.Font.VAR_TYPE_AXES_NONE));
         }
         FontConfig.FontFamily family = new FontConfig.FontFamily(resolvedFonts,
-                LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT,
-                VARIABLE_FONT_FAMILY_TYPE_NONE);
+                LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT);
         return new FontConfig.NamedFamilyList(Collections.singletonList(family),
                 fontFamily.getName());
     }
diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
index 30a4f31..3c3cfe6 100644
--- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
+++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
@@ -45,6 +45,13 @@
     @VisibleForTesting
     static final int STATE_WAITING_FOR_REPORT_POWER_STATUS = 1;
 
+    // State in which the action is delayed. If the action starts and
+    // {@link PowerManager#isInteractive} returns false, it could indicate the beginning of a
+    // standby process. In this scenario, the action will be removed when
+    // {@link HdmiCecLocalDeviceSource#disableDevice} is called, therefore we delay the action.
+    @VisibleForTesting
+    static final int STATE_CHECK_STANDBY_PROCESS_STARTED = 2;
+
     // The maximum number of times we send <Give Device Power Status> before we give up.
     // We wait up to RESPONSE_TIMEOUT_MS * LOOP_COUNTER_MAX = 20 seconds.
     private static final int LOOP_COUNTER_MAX = 10;
@@ -87,6 +94,22 @@
     boolean start() {
         // Because only source device can create this action, it's safe to cast.
         mSource = source();
+
+        if (!mSource.mService.getPowerManager().isInteractive()) {
+            Slog.d(TAG, "PowerManager is not interactive. Delay the action to check if standby"
+                    + " started!");
+            mState = STATE_CHECK_STANDBY_PROCESS_STARTED;
+            addTimer(mState, HdmiConfig.TIMEOUT_MS);
+        } else {
+            startAction();
+        }
+
+        return true;
+    }
+
+    private void startAction() {
+        Slog.i(TAG, "Start action.");
+
         sendCommand(HdmiCecMessageBuilder.buildTextViewOn(getSourceAddress(), mTargetAddress));
 
         boolean is20TargetOnBefore = mIsCec20 && getTargetDevicePowerStatus(mSource, mTargetAddress,
@@ -116,12 +139,11 @@
                     maySendActiveSource();
                 }
                 finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
-                return true;
+                return;
             }
         }
         mState = STATE_WAITING_FOR_REPORT_POWER_STATUS;
         addTimer(mState, HdmiConfig.TIMEOUT_MS);
-        return true;
     }
 
     private void setAndBroadcastActiveSource() {
@@ -174,14 +196,22 @@
         if (mState != state) {
             return;
         }
-        if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) {
-            if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) {
-                queryDevicePowerStatus();
-                addTimer(mState, HdmiConfig.TIMEOUT_MS);
-            } else {
-                // Couldn't wake up the TV for whatever reason. Report failure.
-                finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
-            }
+        switch (state) {
+            case STATE_WAITING_FOR_REPORT_POWER_STATUS:
+                if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) {
+                    queryDevicePowerStatus();
+                    addTimer(mState, HdmiConfig.TIMEOUT_MS);
+                } else {
+                    // Couldn't wake up the TV for whatever reason. Report failure.
+                    finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
+                }
+                return;
+            case STATE_CHECK_STANDBY_PROCESS_STARTED:
+                Slog.d(TAG, "Action was not removed, start the action.");
+                startAction();
+                return;
+            default:
+                return;
         }
     }
 
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 165dfe4..5ffc380 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -64,6 +64,8 @@
                         (reason) -> updateTouchpadNaturalScrollingEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK),
                         (reason) -> updateTouchpadTapToClickEnabled()),
+                Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_DRAGGING),
+                        (reason) -> updateTouchpadTapDraggingEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE),
                         (reason) -> updateTouchpadRightClickZoneEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
@@ -158,6 +160,10 @@
         mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext));
     }
 
+    private void updateTouchpadTapDraggingEnabled() {
+        mNative.setTouchpadTapDraggingEnabled(InputSettings.useTouchpadTapDragging(mContext));
+    }
+
     private void updateTouchpadRightClickZoneEnabled() {
         mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext));
     }
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index bc82078..e5f3484 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -129,6 +129,8 @@
 
     void setTouchpadTapToClickEnabled(boolean enabled);
 
+    void setTouchpadTapDraggingEnabled(boolean enabled);
+
     void setTouchpadRightClickZoneEnabled(boolean enabled);
 
     void setShowTouches(boolean enabled);
@@ -377,6 +379,9 @@
         public native void setTouchpadTapToClickEnabled(boolean enabled);
 
         @Override
+        public native void setTouchpadTapDraggingEnabled(boolean enabled);
+
+        @Override
         public native void setTouchpadRightClickZoneEnabled(boolean enabled);
 
         @Override
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index cc205d4..cc58f38 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -1541,8 +1541,14 @@
      */
     public @NonNull AuthenticationResult unlockTokenBasedProtector(
             IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) {
-        SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME,
-                    protectorId, userId));
+        byte[] data = loadState(SP_BLOB_NAME, protectorId, userId);
+        if (data == null) {
+            AuthenticationResult result = new AuthenticationResult();
+            result.gkResponse = VerifyCredentialResponse.ERROR;
+            Slogf.w(TAG, "spblob not found for protector %016x, user %d", protectorId, userId);
+            return result;
+        }
+        SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(data);
         return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, blob.mProtectorType,
                 token, userId);
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 425659e..4da2cc9 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -10869,6 +10869,7 @@
             ArrayList<Notification.Action> smartActions = record.getSystemGeneratedSmartActions();
             ArrayList<CharSequence> smartReplies = record.getSmartReplies();
             if (redactSensitiveNotificationsFromUntrustedListeners()
+                    && info != null
                     && !mListeners.isUidTrusted(info.uid)
                     && mListeners.hasSensitiveContent(record)) {
                 smartActions = null;
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index fb95632..41ff415 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -210,6 +210,9 @@
 
         mDefaultConfig = readDefaultConfig(mContext.getResources());
         updateDefaultAutomaticRuleNames();
+        if (Flags.modesApi()) {
+            updateDefaultAutomaticRulePolicies();
+        }
         mConfig = mDefaultConfig.copy();
         synchronized (mConfigsArrayLock) {
             mConfigs.put(UserHandle.USER_SYSTEM, mConfig);
@@ -1966,6 +1969,21 @@
         }
     }
 
+    // Updates the policies in the default automatic rules (provided via default XML config) to
+    // be fully filled in default values.
+    private void updateDefaultAutomaticRulePolicies() {
+        if (!Flags.modesApi()) {
+            // Should be checked before calling, but just in case.
+            return;
+        }
+        ZenPolicy defaultPolicy = mDefaultConfig.toZenPolicy();
+        for (ZenRule rule : mDefaultConfig.automaticRules.values()) {
+            if (ZenModeConfig.DEFAULT_RULE_IDS.contains(rule.id) && rule.zenPolicy == null) {
+                rule.zenPolicy = defaultPolicy.copy();
+            }
+        }
+    }
+
     @VisibleForTesting
     protected void applyRestrictions() {
         final boolean zenOn = mZenMode != Global.ZEN_MODE_OFF;
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 47967db..dbff442 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -63,4 +63,11 @@
   namespace: "systemui"
   description: "Timing test, no functionality"
   bug: "316931130"
+}
+
+flag {
+  name: "notification_custom_view_uri_restriction"
+  namespace: "systemui"
+  description: "This flag enables memory restriction of notifications holding custom views with Uri Bitmaps"
+  bug: "270553691"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 0555d90..0be8e6e 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -586,7 +586,7 @@
                     list.add(ri);
                     PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                             mInjector.getCompatibility(), mComponentResolver,
-                            list, false, intent, resolvedType, flags, filterCallingUid);
+                            list, false, intent, resolvedType, filterCallingUid);
                 }
             }
         } else {
@@ -616,7 +616,7 @@
             // We also have to ensure all components match the original intent
             PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                     mInjector.getCompatibility(), mComponentResolver,
-                    list, false, originalIntent, resolvedType, flags, filterCallingUid);
+                    list, false, originalIntent, resolvedType, filterCallingUid);
         }
 
         return skipPostResolution ? list : applyPostResolutionFilter(
@@ -700,7 +700,7 @@
                     list.add(ri);
                     PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                             mInjector.getCompatibility(), mComponentResolver,
-                            list, false, intent, resolvedType, flags, callingUid);
+                            list, false, intent, resolvedType, callingUid);
                 }
             }
         } else {
@@ -712,7 +712,7 @@
             // We also have to ensure all components match the original intent
             PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                     mInjector.getCompatibility(), mComponentResolver,
-                    list, false, originalIntent, resolvedType, flags, callingUid);
+                    list, false, originalIntent, resolvedType, callingUid);
         }
 
         return list;
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index a10bae9..3abf3a5 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -16,7 +16,10 @@
 
 package com.android.server.pm;
 
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_SETUP;
 import static android.os.Trace.TRACE_TAG_DALVIK;
+import static android.os.incremental.IncrementalManager.isIncrementalPath;
 
 import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
 import static com.android.server.pm.ApexManager.ActiveApexInfo;
@@ -27,6 +30,8 @@
 import static com.android.server.pm.PackageManagerService.REASON_BOOT_AFTER_OTA;
 import static com.android.server.pm.PackageManagerService.REASON_CMDLINE;
 import static com.android.server.pm.PackageManagerService.REASON_FIRST_BOOT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_APEX;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_INSTANT_APP;
 import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
 import static com.android.server.pm.PackageManagerService.TAG;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
@@ -45,6 +50,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.SharedLibraryInfo;
 import android.content.pm.dex.ArtManager;
@@ -54,6 +61,7 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.provider.Settings.Global;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
@@ -1091,4 +1099,73 @@
                         + " has unsupported status " + status);
         }
     }
+
+    /**
+     * Returns DexoptOptions by the given InstallRequest.
+     */
+    static DexoptOptions getDexoptOptionsByInstallRequest(InstallRequest installRequest,
+            DexManager dexManager) {
+        final PackageSetting ps = installRequest.getScannedPackageSetting();
+        final String packageName = ps.getPackageName();
+        final boolean isBackupOrRestore =
+                installRequest.getInstallReason() == INSTALL_REASON_DEVICE_RESTORE
+                        || installRequest.getInstallReason() == INSTALL_REASON_DEVICE_SETUP;
+        final int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
+                | DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
+                | DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE
+                | (isBackupOrRestore ? DexoptOptions.DEXOPT_FOR_RESTORE : 0);
+        // Compute the compilation reason from the installation scenario.
+        final int compilationReason =
+                dexManager.getCompilationReasonForInstallScenario(
+                        installRequest.getInstallScenario());
+        return new DexoptOptions(packageName, compilationReason, dexoptFlags);
+    }
+
+    /**
+     * Use ArtService to perform dexopt by the given InstallRequest.
+     */
+    static DexoptResult dexoptPackageUsingArtService(InstallRequest installRequest,
+            DexoptOptions dexoptOptions) {
+        final PackageSetting ps = installRequest.getScannedPackageSetting();
+        final String packageName = ps.getPackageName();
+
+        PackageManagerLocal packageManagerLocal =
+                LocalManagerRegistry.getManager(PackageManagerLocal.class);
+        try (PackageManagerLocal.FilteredSnapshot snapshot =
+                     packageManagerLocal.withFilteredSnapshot()) {
+            boolean ignoreDexoptProfile =
+                    (installRequest.getInstallFlags()
+                            & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
+                            != 0;
+            /*@DexoptFlags*/ int extraFlags =
+                    ignoreDexoptProfile && Flags.useArtServiceV2()
+                            ? ArtFlags.FLAG_IGNORE_PROFILE
+                            : 0;
+            DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
+            DexoptResult dexOptResult = getArtManagerLocal().dexoptPackage(
+                    snapshot, packageName, params);
+
+            return dexOptResult;
+        }
+    }
+
+    /**
+     * Returns whether to perform dexopt by the given InstallRequest.
+     */
+    static boolean shouldPerformDexopt(InstallRequest installRequest, DexoptOptions dexoptOptions,
+            Context context) {
+        final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
+        final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
+        final PackageSetting ps = installRequest.getScannedPackageSetting();
+        final AndroidPackage pkg = ps.getPkg();
+        final boolean onIncremental = isIncrementalPath(ps.getPathString());
+
+        return (!instantApp || Global.getInt(context.getContentResolver(),
+                Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
+                && pkg != null
+                && !pkg.isDebuggable()
+                && (!onIncremental)
+                && dexoptOptions.isCompilationEnabled()
+                && !isApex;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index e70c5ea..28f3d59 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -32,8 +32,6 @@
 import static android.content.pm.PackageManager.INSTALL_FAILED_TEST_ONLY;
 import static android.content.pm.PackageManager.INSTALL_FAILED_UID_CHANGED;
 import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
-import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
-import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_SETUP;
 import static android.content.pm.PackageManager.INSTALL_STAGED;
 import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
 import static android.content.pm.PackageManager.UNINSTALL_REASON_UNKNOWN;
@@ -170,10 +168,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
 import com.android.server.EventLogTags;
-import com.android.server.LocalManagerRegistry;
 import com.android.server.SystemConfig;
-import com.android.server.art.model.ArtFlags;
-import com.android.server.art.model.DexoptParams;
 import com.android.server.art.model.DexoptResult;
 import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.pm.Installer.LegacyDexoptDisabledException;
@@ -800,10 +795,27 @@
                     "restoreAndPostInstall userId=" + userId + " package=" + request.getPkg());
         }
 
-        // A restore should be requested at this point if (a) the install
-        // succeeded, (b) the operation is not an update.
+        PackageSetting packageSetting = null;
+
         final boolean update = request.isUpdate();
-        boolean doRestore = !update && request.getPkg() != null;
+        boolean doRestore = false;
+        if (request.getPkg() != null && !request.isArchived()) {
+            // A restore should be requested at this point:
+            // if the install succeeded and it's not an archived install
+            if (!update) {
+                // AND the operation is not an update,
+                doRestore = true;
+            } else {
+                // OR the package has never been restored.
+                String packageName = request.getPkg().getPackageName();
+                synchronized (mPm.mLock) {
+                    packageSetting = mPm.mSettings.getPackageLPr(packageName);
+                    if (packageSetting != null && packageSetting.isPendingRestore()) {
+                        doRestore = true;
+                    }
+                }
+            }
+        }
 
         // Set up the post-install work request bookkeeping.  This will be used
         // and cleaned up by the post-install event handling regardless of whether
@@ -833,7 +845,13 @@
             doRestore = performRollbackManagerRestore(userId, token, request);
         }
 
-        if (!doRestore) {
+        if (doRestore) {
+            if (packageSetting != null) {
+                synchronized (mPm.mLock) {
+                    packageSetting.setPendingRestore(false);
+                }
+            }
+        } else {
             // No restore possible, or the Backup Manager was mysteriously not
             // available -- just fire the post-install work request directly.
             if (DEBUG_INSTALL) Log.v(TAG, "No restore - queue post-install for " + token);
@@ -2464,8 +2482,6 @@
         final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
         for (ReconciledPackage reconciledPkg : reconciledPackages) {
             final InstallRequest installRequest = reconciledPkg.mInstallRequest;
-            final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
-            final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
             final PackageSetting ps = installRequest.getScannedPackageSetting();
             final String packageName = ps.getPackageName();
             final String codePath = ps.getPathString();
@@ -2507,28 +2523,14 @@
                 }
             }
 
-            // Compute the compilation reason from the installation scenario.
-            final int compilationReason =
-                    mDexManager.getCompilationReasonForInstallScenario(
-                            installRequest.getInstallScenario());
-
             // Construct the DexoptOptions early to see if we should skip running dexopt.
             //
             // Do not run PackageDexOptimizer through the local performDexOpt
             // method because `pkg` may not be in `mPackages` yet.
             //
             // Also, don't fail application installs if the dexopt step fails.
-            final boolean isBackupOrRestore =
-                    installRequest.getInstallReason() == INSTALL_REASON_DEVICE_RESTORE
-                            || installRequest.getInstallReason() == INSTALL_REASON_DEVICE_SETUP;
-
-            final int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
-                    | DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
-                    | DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE
-                    | (isBackupOrRestore ? DexoptOptions.DEXOPT_FOR_RESTORE : 0);
-            DexoptOptions dexoptOptions =
-                    new DexoptOptions(packageName, compilationReason, dexoptFlags);
-
+            DexoptOptions dexoptOptions = DexOptHelper.getDexoptOptionsByInstallRequest(
+                    installRequest, mDexManager);
             // Check whether we need to dexopt the app.
             //
             // NOTE: it is IMPORTANT to call dexopt:
@@ -2555,16 +2557,9 @@
             //
             // TODO(b/174695087): instantApp and onIncremental should be removed and their install
             //       path moved to SCENARIO_FAST.
-            final boolean performDexopt =
-                    (!instantApp || android.provider.Settings.Global.getInt(
-                            mContext.getContentResolver(),
-                            android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
-                            && pkg != null
-                            && !pkg.isDebuggable()
-                            && (!onIncremental)
-                            && dexoptOptions.isCompilationEnabled()
-                            && !isApex;
 
+            final boolean performDexopt = DexOptHelper.shouldPerformDexopt(installRequest,
+                    dexoptOptions, mContext);
             if (performDexopt) {
                 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
 
@@ -2579,23 +2574,9 @@
                 realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
 
                 if (useArtService()) {
-                    PackageManagerLocal packageManagerLocal =
-                            LocalManagerRegistry.getManager(PackageManagerLocal.class);
-                    try (PackageManagerLocal.FilteredSnapshot snapshot =
-                                    packageManagerLocal.withFilteredSnapshot()) {
-                        boolean ignoreDexoptProfile =
-                                (installRequest.getInstallFlags()
-                                        & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
-                                != 0;
-                        /*@DexoptFlags*/ int extraFlags =
-                                ignoreDexoptProfile && Flags.useArtServiceV2()
-                                ? ArtFlags.FLAG_IGNORE_PROFILE
-                                : 0;
-                        DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
-                        DexoptResult dexOptResult = DexOptHelper.getArtManagerLocal().dexoptPackage(
-                                snapshot, packageName, params);
-                        installRequest.onDexoptFinished(dexOptResult);
-                    }
+                    DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService(
+                            installRequest, dexoptOptions);
+                    installRequest.onDexoptFinished(dexOptResult);
                 } else {
                     try {
                         mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
@@ -2919,7 +2900,7 @@
                     mPm.scheduleDeferredNoKillPostDelete(args);
                     if (Flags.improveInstallDontKill()) {
                         synchronized (mPm.mInstallLock) {
-                            PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller,
+                            PackageManagerServiceUtils.linkFilesToOldDirs(mPm.mInstaller,
                                     packageName, pkgSetting.getPath(), pkgSetting.getOldPaths());
                         }
                     }
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index b5346a3..43328fc 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,6 +18,10 @@
 
 import static android.Manifest.permission.READ_FRAME_BUFFER;
 import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY;
+import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -43,6 +47,7 @@
 import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions;
 import android.app.AppGlobals;
+import android.app.AppOpsManager;
 import android.app.IApplicationThread;
 import android.app.PendingIntent;
 import android.app.admin.DevicePolicyCache;
@@ -213,6 +218,7 @@
         private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
         private final ShortcutServiceInternal mShortcutServiceInternal;
         private final PackageManagerInternal mPackageManagerInternal;
+        private final AppOpsManager mAppOpsManager;
         private final PackageCallbackList<IOnAppsChangedListener> mListeners
                 = new PackageCallbackList<IOnAppsChangedListener>();
         private final DevicePolicyManager mDpm;
@@ -253,6 +259,7 @@
                     LocalServices.getService(ShortcutServiceInternal.class));
             mPackageManagerInternal = Objects.requireNonNull(
                     LocalServices.getService(PackageManagerInternal.class));
+            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
             mShortcutServiceInternal.addListener(mPackageMonitor);
             mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal);
             mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler);
@@ -1719,7 +1726,8 @@
                                     .scheme("android-app")
                                     .authority(callingPackage)
                                     .build())
-                    .setPackage(installerPackageName);
+                    .setPackage(installerPackageName)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
 
         @Override
@@ -1997,6 +2005,23 @@
             }
         }
 
+        @Override
+        public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+                boolean enableUnarchivalConfirmation) {
+            int callingUid = Binder.getCallingUid();
+            Binder.withCleanCallingIdentity(
+                    () -> {
+                        mAppOpsManager.setUidMode(
+                                OP_ARCHIVE_ICON_OVERLAY,
+                                callingUid,
+                                enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED);
+                        mAppOpsManager.setUidMode(
+                                OP_UNARCHIVAL_CONFIRMATION,
+                                callingUid,
+                                enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED);
+                    });
+        }
+
         /** Checks if user is a profile of or same as listeningUser.
          * and the user is enabled. */
         private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 32f5646..474b590 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -20,6 +20,7 @@
 import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
 import static android.app.ActivityManager.START_PERMISSION_DENIED;
 import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_IGNORED;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap;
@@ -275,11 +276,12 @@
         Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
 
         try {
-            // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options.
             requestUnarchive(packageName, callerPackageName,
                     getOrCreateLauncherListener(userId, packageName),
                     UserHandle.of(userId),
-                    false /* showUnarchivalConfirmation= */);
+                    getAppOpsManager().checkOp(
+                            AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName)
+                            == MODE_ALLOWED);
         } catch (Throwable t) {
             Slog.e(TAG, TextUtils.formatSimple(
                     "Unexpected error occurred while unarchiving package %s: %s.", packageName,
@@ -796,7 +798,8 @@
      * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple
      * launcher activities, only one of the icons is returned arbitrarily.
      */
-    public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
+    public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+            String callingPackageName) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(user);
 
@@ -819,7 +822,13 @@
         // TODO(b/298452477) Handle monochrome icons.
         // In the rare case the archived app defined more than two launcher activities, we choose
         // the first one arbitrarily.
-        return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0)));
+        Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0));
+        if (getAppOpsManager().checkOp(
+                AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName)
+                == MODE_ALLOWED) {
+            icon = includeCloudOverlay(icon);
+        }
+        return icon;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9617098..b705e84 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -598,6 +598,8 @@
 
     static final int DEFAULT_FILE_ACCESS_MODE = 0644;
 
+    static final int DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE = 0755;
+
     final Handler mHandler;
     final Handler mBackgroundHandler;
 
@@ -1550,6 +1552,8 @@
             }
             pkgSetting
                     .setPkg(null)
+                    // This package was installed as archived. Need to mark it for later restore.
+                    .setPendingRestore(true)
                     .modifyUserState(userId)
                     .setInstalled(false)
                     .setArchiveState(archiveState);
@@ -6414,8 +6418,10 @@
         }
 
         @Override
-        public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
-            return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user);
+        public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+                @NonNull String callingPackageName) {
+            return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user,
+                    callingPackageName);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 8531692..4f9ed03 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -23,6 +23,7 @@
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDWR;
 
+import static com.android.internal.content.NativeLibraryHelper.LIB64_DIR_NAME;
 import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME;
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH;
 import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
@@ -31,6 +32,7 @@
 import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING;
 import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
 import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
+import static com.android.server.pm.PackageManagerService.DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE;
 import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX;
 import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX;
 import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
@@ -44,7 +46,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.Overridable;
 import android.content.Context;
 import android.content.Intent;
@@ -198,7 +200,7 @@
      */
     @Overridable
     @ChangeId
-    @Disabled  /* Enforcement reverted in T: b/274147456 */
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188;
 
     /**
@@ -1192,8 +1194,7 @@
     public static void applyEnforceIntentFilterMatching(
             PlatformCompat compat, ComponentResolverApi resolver,
             List<ResolveInfo> resolveInfos, boolean isReceiver,
-            Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
-            int filterCallingUid) {
+            Intent intent, String resolvedType, int filterCallingUid) {
         if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return;
 
         // Do not enforce filter matching when the caller is system or root
@@ -1203,10 +1204,9 @@
                 ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM)
                 : null;
 
-        final boolean defaultOnly = (flags & PackageManager.MATCH_DEFAULT_ONLY) != 0;
-
-        final boolean enforce = compat.isChangeEnabledByUidInternal(
-                ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, filterCallingUid);
+        final boolean enforce = android.security.Flags.enforceIntentFilterMatch()
+                && compat.isChangeEnabledByUidInternal(
+                        ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, filterCallingUid);
 
         for (int i = resolveInfos.size() - 1; i >= 0; --i) {
             final ComponentInfo info = resolveInfos.get(i).getComponentInfo();
@@ -1237,8 +1237,7 @@
             boolean match = false;
             for (int j = 0, size = comp.getIntents().size(); j < size; ++j) {
                 IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter();
-                if (IntentResolver.intentMatchesFilter(
-                        intentFilter, intent, resolvedType, defaultOnly)) {
+                if (IntentResolver.intentMatchesFilter(intentFilter, intent, resolvedType)) {
                     match = true;
                     break;
                 }
@@ -1557,7 +1556,7 @@
         return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName);
     }
 
-    public static void linkSplitsToOldDirs(@NonNull Installer installer,
+    public static void linkFilesToOldDirs(@NonNull Installer installer,
                                            @NonNull String packageName,
                                            @NonNull File newPath,
                                            @Nullable Set<File> oldPaths) {
@@ -1572,56 +1571,108 @@
         if (filesInNewPath == null || filesInNewPath.length == 0) {
             return;
         }
-        final List<String> splitApkNames = new ArrayList<String>();
-        for (int i = 0; i < filesInNewPath.length; i++) {
-            if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) {
-                splitApkNames.add(filesInNewPath[i].getName());
+        final List<File> splitApks = new ArrayList<>();
+        for (File file : filesInNewPath) {
+            if (!file.isDirectory() && file.toString().endsWith(".apk")) {
+                splitApks.add(file);
             }
         }
-        final int numSplits = splitApkNames.size();
-        if (numSplits == 0) {
+        if (splitApks.isEmpty()) {
             return;
         }
+        final File[] splitApkNames = splitApks.toArray(new File[0]);
         for (File oldPath : oldPaths) {
             if (!oldPath.exists()) {
                 continue;
             }
-            for (int i = 0; i < numSplits; i++) {
-                final String splitApkName = splitApkNames.get(i);
-                final File linkedSplit = new File(oldPath, splitApkName);
-                if (linkedSplit.exists()) {
-                    if (DEBUG) {
-                        Slog.d(PackageManagerService.TAG, "Skipping existing linked split <"
-                                + linkedSplit + ">");
-                    }
-                    continue;
-                }
-                final File sourceSplit = new File(newPath, splitApkName);
-                try {
-                    installer.linkFile(packageName, splitApkName,
-                            newPath.getAbsolutePath(), oldPath.getAbsolutePath());
-                    if (DEBUG) {
-                        Slog.d(PackageManagerService.TAG, "Linked <"
-                                + sourceSplit + "> to <" + linkedSplit + ">");
-                    }
-                } catch (Installer.InstallerException e) {
-                    Slog.w(PackageManagerService.TAG, "Failed to link split <"
-                            + sourceSplit + " > to <" + linkedSplit + ">", e);
-                    continue;
-                }
-                try {
-                    Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
-                } catch (ErrnoException e) {
-                    Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <"
-                            + linkedSplit + ">", e);
-                    continue;
-                }
-                if (!SELinux.restorecon(linkedSplit)) {
-                    Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <"
-                            + linkedSplit + ">");
-                }
+            linkFilesAndSetModes(installer, packageName, newPath, oldPath, splitApkNames,
+                    DEFAULT_FILE_ACCESS_MODE);
+            linkNativeLibraries(installer, packageName, newPath, oldPath, LIB_DIR_NAME);
+            linkNativeLibraries(installer, packageName, newPath, oldPath, LIB64_DIR_NAME);
+        }
+
+    }
+
+    private static void linkNativeLibraries(@NonNull Installer installer,
+                                            @NonNull String packageName,
+                                            @NonNull File sourcePath, @NonNull File targetPath,
+                                            @NonNull String libDirName) {
+        final File sourceLibDir = new File(sourcePath, libDirName);
+        if (!sourceLibDir.exists()) {
+            return;
+        }
+        final File targetLibDir = new File(targetPath, libDirName);
+        if (!targetLibDir.exists()) {
+            try {
+                NativeLibraryHelper.createNativeLibrarySubdir(targetLibDir);
+            } catch (IOException e) {
+                Slog.w(PackageManagerService.TAG, "Failed to create native library dir at <"
+                        + targetLibDir + ">", e);
+                return;
             }
         }
-        //TODO(b/291212866): support native libs
+        final File[] archs = sourceLibDir.listFiles();
+        if (archs == null) {
+            return;
+        }
+        for (File arch : archs) {
+            final File targetArchDir = new File(targetLibDir, arch.getName());
+            if (!targetArchDir.exists()) {
+                try {
+                    NativeLibraryHelper.createNativeLibrarySubdir(targetArchDir);
+                } catch (IOException e) {
+                    Slog.w(PackageManagerService.TAG, "Failed to create native library subdir at <"
+                            + targetArchDir + ">", e);
+                    continue;
+                }
+            }
+            final File sourceArchDir = new File(sourceLibDir, arch.getName());
+            final File[] files = sourceArchDir.listFiles();
+            if (files == null || files.length == 0) {
+                continue;
+            }
+            linkFilesAndSetModes(installer, packageName, sourceArchDir, targetArchDir, files,
+                    DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE);
+        }
+    }
+
+    // Link the files with specified names from under the sourcePath to be under the targetPath
+    private static void linkFilesAndSetModes(@NonNull Installer installer, String packageName,
+            @NonNull File sourcePath, @NonNull File targetPath, @NonNull File[] files, int mode) {
+        for (File file : files) {
+            final String fileName = file.getName();
+            final File sourceFile = new File(sourcePath, fileName);
+            final File targetFile = new File(targetPath, fileName);
+            if (targetFile.exists()) {
+                if (DEBUG) {
+                    Slog.d(PackageManagerService.TAG, "Skipping existing linked file <"
+                            + targetFile + ">");
+                }
+                continue;
+            }
+            try {
+                installer.linkFile(packageName, fileName,
+                        sourcePath.getAbsolutePath(), targetPath.getAbsolutePath());
+                if (DEBUG) {
+                    Slog.d(PackageManagerService.TAG, "Linked <"
+                            + sourceFile + "> to <" + targetFile + ">");
+                }
+            } catch (Installer.InstallerException e) {
+                Slog.w(PackageManagerService.TAG, "Failed to link native library <"
+                        + sourceFile + "> to <" + targetFile + ">", e);
+                continue;
+            }
+            try {
+                Os.chmod(targetFile.getAbsolutePath(), mode);
+            } catch (ErrnoException e) {
+                Slog.w(PackageManagerService.TAG, "Failed to set mode for linked file <"
+                        + targetFile + ">", e);
+                continue;
+            }
+            if (!SELinux.restorecon(targetFile)) {
+                Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked file <"
+                        + targetFile + ">");
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 74c482b..f474d32 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -94,7 +94,8 @@
                 INSTALL_PERMISSION_FIXED,
                 UPDATE_AVAILABLE,
                 FORCE_QUERYABLE_OVERRIDE,
-                SCANNED_AS_STOPPED_SYSTEM_APP
+                SCANNED_AS_STOPPED_SYSTEM_APP,
+                PENDING_RESTORE,
         })
         public @interface Flags {
         }
@@ -102,6 +103,7 @@
         private static final int UPDATE_AVAILABLE = 1 << 1;
         private static final int FORCE_QUERYABLE_OVERRIDE = 1 << 2;
         private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3;
+        private static final int PENDING_RESTORE = 1 << 4;
     }
     private int mBooleans;
 
@@ -543,6 +545,20 @@
         return mSharedUserAppId > 0;
     }
 
+    /**
+     * @see PackageState#isPendingRestore()
+     */
+    public PackageSetting setPendingRestore(boolean value) {
+        setBoolean(Booleans.PENDING_RESTORE, value);
+        onChanged();
+        return this;
+    }
+
+    @Override
+    public boolean isPendingRestore() {
+        return getBoolean(Booleans.PENDING_RESTORE);
+    }
+
     @Override
     public String toString() {
         return "PackageSetting{"
diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
index 203e1de..b664e39 100644
--- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java
+++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
@@ -459,7 +459,7 @@
                     list.add(ri);
                     PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                             mPlatformCompat, componentResolver, list, true, intent,
-                            resolvedType, flags, filterCallingUid);
+                            resolvedType, filterCallingUid);
                 }
             }
         } else {
@@ -485,7 +485,7 @@
             // We also have to ensure all components match the original intent
             PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
                     mPlatformCompat, componentResolver,
-                    list, true, originalIntent, resolvedType, flags, filterCallingUid);
+                    list, true, originalIntent, resolvedType, filterCallingUid);
         }
 
         return computer.applyPostResolutionFilter(list, instantAppPkgName, false, queryingUid,
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 7cf1d33..c7ee649 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4537,6 +4537,8 @@
         t.traceBegin("createNewUser-" + userHandle);
         Installer.Batch batch = new Installer.Batch();
         final boolean skipPackageAllowList = userTypeInstallablePackages == null;
+        // Use the same timestamp for all system apps that are to be installed on the new user
+        final long currentTimeMillis = System.currentTimeMillis();
         synchronized (mLock) {
             final int size = mPackages.size();
             for (int i = 0; i < size; i++) {
@@ -4552,6 +4554,9 @@
                                 ps.getPackageName()));
                 // Only system apps are initially installed.
                 ps.setInstalled(shouldReallyInstall, userHandle);
+                if (Flags.fixSystemAppsFirstInstallTime() && shouldReallyInstall) {
+                    ps.setFirstInstallTime(currentTimeMillis, userHandle);
+                }
 
                 // Non-Apex system apps, that are not included in the allowlist in
                 // initialNonStoppedSystemPackages, should be marked as stopped by default.
@@ -4878,7 +4883,7 @@
 
     @NeverCompile // Avoid size overhead of debugging code.
     void dumpPackageLPr(PrintWriter pw, String prefix, String checkinTag,
-            ArraySet<String> permissionNames, PackageSetting ps,
+            ArraySet<String> permissionNames, @NonNull PackageSetting ps,
             LegacyPermissionState permissionsState, SimpleDateFormat sdf, Date date,
             List<UserInfo> users, boolean dumpAll, boolean dumpAllComponents) {
         AndroidPackage pkg = ps.getPkg();
@@ -5017,6 +5022,10 @@
                 pw.print(prefix); pw.print("  privateFlags="); printFlags(pw,
                         privateFlags, PRIVATE_FLAG_DUMP_SPEC); pw.println();
             }
+            if (ps.isPendingRestore()) {
+                pw.print(prefix); pw.print("  pendingRestore=true");
+                pw.println();
+            }
             if (!pkg.isUpdatableSystem()) {
                 pw.print(prefix); pw.print("  updatableSystem=false");
                 pw.println();
@@ -5227,6 +5236,9 @@
         pw.print(prefix); pw.print("  privatePkgFlags="); printFlags(pw, ps.getPrivateFlags(),
                 PRIVATE_FLAG_DUMP_SPEC);
         pw.println();
+        if (ps.isPendingRestore()) {
+            pw.print(prefix); pw.println("  pendingRestore=true");
+        }
         pw.print(prefix); pw.print("  apexModuleName="); pw.println(ps.getApexModuleName());
 
         if (pkg != null && pkg.getOverlayTarget() != null) {
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index a7ae4eb..e0ee199 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -265,6 +265,14 @@
      */
     boolean hasSharedUser();
 
+
+    /**
+     * Whether this app needs to be restore during next install/update.
+     * E.g. if an app was installed as archived and never had a chance to restore its data.
+     * @hide
+     */
+    boolean isPendingRestore();
+
     /**
      * Retrieves the shared user app ID. Note that the actual shared user data is not available here
      * and must be queried separately.
diff --git a/services/core/java/com/android/server/pm/verify/domain/OWNERS b/services/core/java/com/android/server/pm/verify/domain/OWNERS
index c669112..b451fe4 100644
--- a/services/core/java/com/android/server/pm/verify/domain/OWNERS
+++ b/services/core/java/com/android/server/pm/verify/domain/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 36137
+include /PACKAGE_MANAGER_OWNERS
 
-chiuwinson@google.com
-patb@google.com
-toddke@google.com
\ No newline at end of file
+wloh@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index 61348b5..f15646f 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -43,6 +43,7 @@
 import android.app.KeyguardManager;
 import android.app.TaskInfo;
 import android.app.compat.CompatChanges;
+import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
 import android.content.BroadcastReceiver;
@@ -234,7 +235,12 @@
                 this::synchronizeUidPermissionsAndAppOpsAsync);
 
         mAppOpsCallback = new IAppOpsCallback.Stub() {
-            public void opChanged(int op, int uid, @Nullable String packageName) {
+            public void opChanged(int op, int uid, @Nullable String packageName,
+                    String persistentDeviceId) {
+                if (Objects.equals(persistentDeviceId,
+                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+                    return;
+                }
                 if (packageName != null) {
                     synchronizeUidPermissionsAndAppOpsAsync(uid);
                 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fdcc64..51790b8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -75,6 +75,7 @@
 import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED;
 import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
 
+import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
@@ -3779,6 +3780,11 @@
                     sendSystemKeyToStatusBarAsync(event);
                     return true;
                 }
+            case KeyEvent.KEYCODE_SCREENSHOT:
+                if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) {
+                    interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+                }
+                return true;
         }
         if (isValidGlobalKey(keyCode)
                 && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -5022,6 +5028,12 @@
             case KeyEvent.KEYCODE_MACRO_4:
                 result &= ~ACTION_PASS_TO_USER;
                 break;
+            case KeyEvent.KEYCODE_EMOJI_PICKER:
+                if (!emojiAndScreenshotKeycodesAvailable()) {
+                    // Don't allow EMOJI_PICKER key to be dispatched until flag is released.
+                    result &= ~ACTION_PASS_TO_USER;
+                }
+                break;
         }
 
         if (useHapticFeedback) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 01b464f..25e749f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -138,6 +138,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.server.power.optimization.Flags;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 
 import libcore.util.EmptyArray;
@@ -185,7 +186,8 @@
     // TODO: remove "tcp" from network methods, since we measure total stats.
 
     // Current on-disk Parcel version. Must be updated when the format of the parcelable changes
-    public static final int VERSION = 214;
+    public static final int VERSION =
+            !Flags.disableSystemServicePowerAttr() ? 214 : 215;
 
     // The maximum number of names wakelocks we will keep track of
     // per uid; once the limit is reached, we batch the remaining wakelocks
@@ -1753,7 +1755,9 @@
         mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock);
         mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock);
         mKernelWakelockReader = new KernelWakelockReader();
-        mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+        }
         mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats();
         mTmpRailStats = new RailStats();
     }
@@ -11702,7 +11706,9 @@
 
         EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats);
 
-        resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+        }
 
         mNumAllUidCpuTimeReads = 0;
         mNumUidsRemoved = 0;
@@ -13676,7 +13682,9 @@
                     mKernelCpuSpeedReaders[i].readDelta();
                 }
             }
-            mSystemServerCpuThreadReader.readDelta();
+            if (!Flags.disableSystemServicePowerAttr()) {
+                mSystemServerCpuThreadReader.readDelta();
+            }
             return;
         }
 
@@ -15696,23 +15704,25 @@
             }
         }
 
-        updateSystemServiceCallStats();
-        if (mBinderThreadCpuTimesUs != null) {
-            pw.println("Per UID System server binder time in ms:");
-            long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
-            for (int i = 0; i < size; i++) {
-                int u = mUidStats.keyAt(i);
-                Uid uid = mUidStats.get(u);
-                double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
-                long timeUs = 0;
-                for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
-                    timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
-                }
+        if (!Flags.disableSystemServicePowerAttr()) {
+            updateSystemServiceCallStats();
+            if (mBinderThreadCpuTimesUs != null) {
+                pw.println("Per UID System server binder time in ms:");
+                long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
+                for (int i = 0; i < size; i++) {
+                    int u = mUidStats.keyAt(i);
+                    Uid uid = mUidStats.get(u);
+                    double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
+                    long timeUs = 0;
+                    for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
+                        timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
+                    }
 
-                pw.print("  ");
-                pw.print(u);
-                pw.print(": ");
-                pw.println(timeUs / 1000);
+                    pw.print("  ");
+                    pw.print(u);
+                    pw.print(": ");
+                    pw.println(timeUs / 1000);
+                }
             }
         }
     }
@@ -16428,8 +16438,10 @@
             }
         }
 
-        mBinderThreadCpuTimesUs =
-                LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mBinderThreadCpuTimesUs =
+                    LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+        }
     }
 
     /**
@@ -16973,7 +16985,9 @@
             }
         }
 
-        LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+        }
     }
 
     @GuardedBy("this")
@@ -16985,7 +16999,9 @@
         // if we had originally pulled a time before the RTC was set.
         getStartClockTime();
 
-        updateSystemServiceCallStats();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            updateSystemServiceCallStats();
+        }
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index c3221e4..30b80ae 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -96,11 +96,13 @@
                 mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile));
                 mPowerCalculators.add(new UserPowerCalculator());
 
-                // It is important that SystemServicePowerCalculator be applied last,
-                // because it re-attributes some of the power estimated by the other
-                // calculators.
-                mPowerCalculators.add(
-                        new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+                if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+                    // It is important that SystemServicePowerCalculator be applied last,
+                    // because it re-attributes some of the power estimated by the other
+                    // calculators.
+                    mPowerCalculators.add(
+                            new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+                }
             }
         }
         return mPowerCalculators;
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 0f13571..6546646 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -13,3 +13,11 @@
     description: "Feature flag for streamlined battery stats"
     bug: "285646152"
 }
+
+flag {
+    name: "disable_system_service_power_attr"
+    namespace: "backstage_power"
+    description: "Deprecation of system service power re-attribution"
+    bug: "311793616"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
index 6677e7e..1526230 100644
--- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
+++ b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
@@ -36,10 +36,12 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
+import android.location.flags.Flags;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
+import android.util.TypedValue;
 
 import com.android.internal.util.Preconditions;
 import com.android.server.FgThread;
@@ -70,6 +72,10 @@
     private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
     private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser";
 
+    // a package value that will never match against any package (we can't use null since this will
+    // match against any package).
+    private static final String NO_MATCH_PACKAGE = "";
+
     private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
         if (o1 == o2) {
             return 0;
@@ -196,7 +202,19 @@
         Resources resources = context.getResources();
         boolean enableOverlay = resources.getBoolean(enableOverlayResId);
         if (!enableOverlay) {
-            return resources.getString(nonOverlayPackageResId);
+            if (Flags.fixServiceWatcher()) {
+                // we don't use getText() or similar because it won't return null values
+                TypedValue out = new TypedValue();
+                resources.getValue(nonOverlayPackageResId, out, true);
+                CharSequence explicitPackage = out.coerceToString();
+                if (explicitPackage == null) {
+                    return NO_MATCH_PACKAGE;
+                } else {
+                    return explicitPackage.toString();
+                }
+            } else {
+                return resources.getString(nonOverlayPackageResId);
+            }
         } else {
             return null;
         }
@@ -233,6 +251,10 @@
 
     @Override
     public boolean hasMatchingService() {
+        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+            return false;
+        }
+
         int intentQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
         if (mMatchSystemAppsOnly) {
             intentQueryFlags |= MATCH_SYSTEM_ONLY;
@@ -268,6 +290,10 @@
 
     @Override
     public BoundServiceInfo getServiceInfo() {
+        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+            return null;
+        }
+
         BoundServiceInfo bestServiceInfo = null;
 
         // only allow services in the correct direct boot state to match
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 285bcc3..0ffd002 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -2058,7 +2058,8 @@
     }
 
     private void registerCpuCyclesPerThreadGroupCluster() {
-        if (KernelCpuBpfTracking.isSupported()) {
+        if (KernelCpuBpfTracking.isSupported()
+                && !com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
             int tagId = FrameworkStatsLog.CPU_CYCLES_PER_THREAD_GROUP_CLUSTER;
             PullAtomMetadata metadata = new PullAtomMetadata.Builder()
                     .setAdditiveFields(new int[]{3, 4})
@@ -2073,6 +2074,10 @@
     }
 
     int pullCpuCyclesPerThreadGroupCluster(int atomTag, List<StatsEvent> pulledData) {
+        if (com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+            return StatsManager.PULL_SKIP;
+        }
+
         SystemServiceCpuThreadTimes times = LocalServices.getService(BatteryStatsInternal.class)
                 .getSystemServiceCpuThreadTimes();
         if (times == null) {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index b271a03..a4c6959 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.app.ITransientNotificationCallback;
+import android.content.ComponentName;
 import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -241,4 +242,17 @@
      * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher
      */
     void showMediaOutputSwitcher(String packageName);
+
+    /**
+     * Add a tile to the Quick Settings Panel
+     * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+     * @param end if true, the tile will be added at the end. If false, at the beginning.
+     */
+    void addQsTileToFrontOrEnd(ComponentName tile, boolean end);
+
+    /**
+     * Remove the tile from the Quick Settings Panel
+     * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+     */
+    void removeQsTile(ComponentName tile);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index b21721a..4955358 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -89,6 +89,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -834,6 +835,20 @@
                 }
             }
         }
+
+        @Override
+        public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+            if (Flags.a11yQsShortcut()) {
+                StatusBarManagerService.this.addQsTileToFrontOrEnd(tile, end);
+            }
+        }
+
+        @Override
+        public void removeQsTile(ComponentName tile) {
+            if (Flags.a11yQsShortcut()) {
+                StatusBarManagerService.this.remTile(tile);
+            }
+        }
     };
 
     private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
@@ -934,11 +949,26 @@
     }
 
     public void addTile(ComponentName component) {
+        if (Flags.a11yQsShortcut()) {
+            addQsTileToFrontOrEnd(component, false);
+        } else {
+            enforceStatusBarOrShell();
+
+            if (mBar != null) {
+                try {
+                    mBar.addQsTile(component);
+                } catch (RemoteException ex) {
+                }
+            }
+        }
+    }
+
+    private void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
         enforceStatusBarOrShell();
 
         if (mBar != null) {
             try {
-                mBar.addQsTile(component);
+                mBar.addQsTileToFrontOrEnd(tile, end);
             } catch (RemoteException ex) {
             }
         }
diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
index 3abebf8..d102054 100644
--- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java
+++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
@@ -443,6 +443,8 @@
                 mPendingSuccessfulUnlock = false;
             }
 
+            // It's okay to use the "Inner" version of isDeviceLocked since they differ only for
+            // profiles, which cannot be switched to and thus don't support trust agents anyway.
             if (mTrustManagerService.isDeviceLockedInner(mUserId)) {
                 onDeviceLocked();
             } else {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e5a8a6d..2b05993 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -83,7 +83,6 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
-import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -160,7 +159,6 @@
     private final ActivityManager mActivityManager;
     private FingerprintManager mFingerprintManager;
     private FaceManager mFaceManager;
-    private VirtualDeviceManagerInternal mVirtualDeviceManager;
 
     private enum TrustState {
         // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
@@ -190,25 +188,30 @@
             new SparseArray<>();
 
     /**
-     * Stores the locked state for users on the device. There are three different type of users
+     * Stores the locked state for users on the device. There are several different types of users
      * which are handled slightly differently:
      * <ul>
-     *  <li> Users with real keyguard
+     *  <li> Users with real keyguard:
      *  These are users who can be switched to ({@link UserInfo#supportsSwitchToByUser()}). Their
      *  locked state is derived by a combination of user secure state, keyguard state, trust agent
      *  decision and biometric authentication result. These are updated via
      *  {@link #refreshDeviceLockedForUser(int)} and result stored in {@link #mDeviceLockedForUser}.
-     *  <li> Managed profiles with unified challenge
-     *  Managed profile with unified challenge always shares the same locked state as their parent,
+     *  <li> Profiles with unified challenge:
+     *  Profiles with a unified challenge always share the same locked state as their parent,
      *  so their locked state is not recorded in  {@link #mDeviceLockedForUser}. Instead,
      *  {@link ITrustManager#isDeviceLocked(int)} always resolves their parent user handle and
      *  queries its locked state instead.
-     *  <li> Managed profiles with separate challenge
-     *  Locked state for profile with separate challenge is determined by other parts of the
-     *  framework (mostly PowerManager) and pushed to TrustManagerService via
-     *  {@link ITrustManager#setDeviceLockedForUser(int, boolean)}. Although in a corner case when
-     *  the profile has a separate but empty challenge, setting its {@link #mDeviceLockedForUser} to
-     *  {@code false} is actually done by {@link #refreshDeviceLockedForUser(int)}.
+     *  <li> Profiles without unified challenge:
+     *  The locked state for profiles that do not have a unified challenge (e.g. they have a
+     *  separate challenge from their parent, or they have no parent at all) is determined by other
+     *  parts of the framework (mostly PowerManager) and pushed to TrustManagerService via
+     *  {@link ITrustManager#setDeviceLockedForUser(int, boolean)}.
+     *  However, in the case where such a profile has an empty challenge, setting its
+     *  {@link #mDeviceLockedForUser} to {@code false} is actually done by
+     *  {@link #refreshDeviceLockedForUser(int)}.
+     *  (This serves as a corner case for managed profiles with a separate but empty challenge. It
+     *  is always currently the case for Communal profiles, for which having a non-empty challenge
+     *  is not currently supported.)
      * </ul>
      * TODO: Rename {@link ITrustManager#setDeviceLockedForUser(int, boolean)} to
      * {@code setDeviceLockedForProfile} to better reflect its purpose. Unifying
@@ -796,7 +799,7 @@
 
     /**
      * Update the user's locked state. Only applicable to users with a real keyguard
-     * ({@link UserInfo#supportsSwitchToByUser}) and unsecured managed profiles.
+     * ({@link UserInfo#supportsSwitchToByUser}) and unsecured profiles.
      *
      * If this is called due to an unlock operation set unlockedUser to prevent the lock from
      * being prematurely reset for that user while keyguard is still in the process of going away.
@@ -828,7 +831,11 @@
             boolean secure = mLockPatternUtils.isSecure(id);
 
             if (!info.supportsSwitchToByUser()) {
-                if (info.isManagedProfile() && !secure) {
+                if (info.isProfile() && !secure
+                        && !mLockPatternUtils.isProfileWithUnifiedChallenge(id)) {
+                    // Unsecured profiles need to be explicitly set to false.
+                    // However, Unified challenge profiles officially shouldn't have a presence in
+                    // mDeviceLockedForUser at all, since that's not how they're tracked.
                     setDeviceLockedForUser(id, false);
                 }
                 continue;
@@ -1855,6 +1862,7 @@
         }
     }
 
+    /** If the userId has a parent, returns that parent's userId. Otherwise userId is returned. */
     private int resolveProfileParent(int userId) {
         final long identity = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index e434df7..089a886 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -2791,6 +2791,29 @@
         }
 
         @Override
+        public void notifyTvAdSessionData(
+                IBinder sessionToken, String type, Bundle data, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
+                    userId, "notifyTvAdSessionData");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).notifyTvAdSessionData(type, data);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in notifyTvAdSessionData", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public int getClientPid(String sessionId) {
             ensureTunerResourceAccessPermission();
             final long identity = Binder.clearCallingIdentity();
@@ -4322,6 +4345,23 @@
                 }
             }
         }
+
+        @Override
+        public void onTvInputSessionData(String type, Bundle data) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onTvInputSessionData()");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onTvInputSessionData(type, data, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onTvInputSessionData", e);
+                }
+            }
+        }
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index b6d0ca1..ffce50e 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -937,6 +937,41 @@
         }
 
         @Override
+        public void sendAppLinkCommand(String serviceId, Bundle command, int userId) {
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+                    Binder.getCallingUid(), userId, "sendAppLinkCommand");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId);
+                    if (adServiceState == null) {
+                        Slogf.e(TAG, "failed to sendAppLinkCommand - unknown service id "
+                                + serviceId);
+                        return;
+                    }
+                    ComponentName componentName = adServiceState.mInfo.getComponent();
+                    AdServiceState serviceState = userState.mAdServiceStateMap.get(componentName);
+                    if (serviceState == null) {
+                        serviceState = new AdServiceState(componentName, serviceId, resolvedUserId);
+                        serviceState.addPendingAppLinkCommand(command);
+                        userState.mAdServiceStateMap.put(componentName, serviceState);
+                        updateAdServiceConnectionLocked(componentName, resolvedUserId);
+                    } else if (serviceState.mService != null) {
+                        serviceState.mService.sendAppLinkCommand(command);
+                    } else {
+                        serviceState.addPendingAppLinkCommand(command);
+                        updateAdServiceConnectionLocked(componentName, resolvedUserId);
+                    }
+                }
+            } catch (RemoteException e) {
+                Slogf.e(TAG, "error in sendAppLinkCommand", e);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void createSession(final ITvAdClient client, final String serviceId, String type,
                 int seq, int userId) {
             final int callingUid = Binder.getCallingUid();
@@ -1320,6 +1355,32 @@
         }
 
         @Override
+        public void notifyTvInputSessionData(
+                IBinder sessionToken, String type, Bundle data, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyTvInputSessionData(type=%d)", type);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+                    "notifyTvInputSessionData");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        AdSessionState sessionState =
+                                getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getAdSessionLocked(sessionState).notifyTvInputSessionData(type, data);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyTvInputSessionData", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void registerCallback(final ITvAdManagerCallback callback, int userId) {
             int callingPid = Binder.getCallingPid();
             int callingUid = Binder.getCallingUid();
@@ -4152,6 +4213,25 @@
         }
 
         @Override
+        public void onRequestSigning2(String id, String algorithm, String host,
+                int port, byte[] data) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestSigning");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestSigning2(
+                            id, algorithm, host, port, data, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestSigning", e);
+                }
+            }
+        }
+
+        @Override
         public void onRequestCertificate(String host, int port) {
             synchronized (mLock) {
                 if (DEBUG) {
@@ -4339,6 +4419,110 @@
             }
         }
 
+        @Override
+        public void onRequestCurrentVideoBounds() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestCurrentVideoBounds");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestCurrentVideoBounds(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestCurrentVideoBounds", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRequestCurrentChannelUri() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestCurrentChannelUri");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestCurrentChannelUri(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestCurrentChannelUri", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRequestTrackInfoList() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestTrackInfoList");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestTrackInfoList(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestTrackInfoList", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRequestCurrentTvInputId() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestCurrentTvInputId");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestCurrentTvInputId(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestCurrentTvInputId", e);
+                }
+            }
+        }
+
+
+        @Override
+        public void onRequestSigning(String id, String algorithm, String alias, byte[] data) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestSigning");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestSigning(
+                            id, algorithm, alias, data, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestSigning", e);
+                }
+            }
+        }
+
+        @Override
+        public void onTvAdSessionData(String type, Bundle data) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onTvAdSessionData");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onTvAdSessionData(type, data, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onTvAdSessionData", e);
+                }
+            }
+        }
+
         @GuardedBy("mLock")
         private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) {
             try {
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index ed04e5f..1383708 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -34,7 +34,7 @@
     @NonNull private final Looper mLooper;
     @NonNull private final VcnNetworkProvider mVcnNetworkProvider;
     @NonNull private final FeatureFlags mFeatureFlags;
-    @NonNull private final com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+    @NonNull private final android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags;
     private final boolean mIsInTestMode;
 
     public VcnContext(
@@ -49,7 +49,7 @@
 
         // Auto-generated class
         mFeatureFlags = new FeatureFlagsImpl();
-        mCoreNetFeatureFlags = new com.android.net.flags.FeatureFlagsImpl();
+        mCoreNetFeatureFlags = new android.net.platform.flags.FeatureFlagsImpl();
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 0a7872f..c9805c7 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -17,10 +17,10 @@
 package com.android.server.vibrator;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.vibrator.V1_0.EffectStrength;
 import android.os.IExternalVibratorService;
+import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.vibrator.Flags;
@@ -57,8 +57,7 @@
     private final SparseArray<ScaleLevel> mScaleLevels;
     private final VibrationSettings mSettingsController;
     private final int mDefaultVibrationAmplitude;
-
-    private SparseArray<Float> mAdaptiveHapticsScales;
+    private final SparseArray<Float> mAdaptiveHapticsScales = new SparseArray<>();
 
     VibrationScaler(Context context, VibrationSettings settingsController) {
         mSettingsController = settingsController;
@@ -147,7 +146,7 @@
 
             // If adaptive haptics scaling is available for this usage, apply it to the segment.
             if (Flags.adaptiveHapticsEnabled()
-                    && mAdaptiveHapticsScales != null && mAdaptiveHapticsScales.size() > 0
+                    && mAdaptiveHapticsScales.size() > 0
                     && mAdaptiveHapticsScales.contains(usageHint)) {
                 float adaptiveScale = mAdaptiveHapticsScales.get(usageHint);
                 segment = segment.scale(adaptiveScale);
@@ -187,13 +186,35 @@
     }
 
     /**
-     * Updates the adaptive haptics scales.
-     * @param scales the new vibration scales to apply.
+     * Updates the adaptive haptics scales list by adding or modifying the scale for this usage.
+     *
+     * @param usageHint one of VibrationAttributes.USAGE_*.
+     * @param scale The scaling factor that should be applied to vibrations of this usage.
      *
      * @hide
      */
-    public void updateAdaptiveHapticsScales(@Nullable SparseArray<Float> scales) {
-        mAdaptiveHapticsScales = scales;
+    public void updateAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint, float scale) {
+        mAdaptiveHapticsScales.put(usageHint, scale);
+    }
+
+    /**
+     * Removes the usage from the cached adaptive haptics scales list.
+     *
+     * @param usageHint one of VibrationAttributes.USAGE_*.
+     *
+     * @hide
+     */
+    public void removeAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint) {
+        mAdaptiveHapticsScales.remove(usageHint);
+    }
+
+    /**
+     * Removes all cached adaptive haptics scales.
+     *
+     * @hide
+     */
+    public void clearAdaptiveHapticsScales() {
+        mAdaptiveHapticsScales.clear();
     }
 
     /** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 839c207..fab0430 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -376,6 +376,25 @@
     }
 
     /**
+     * Returns the duration, in milliseconds, that the vibrator control service will wait for new
+     * vibration params.
+     * @return The request vibration params timeout in milliseconds.
+     * @hide
+     */
+    public int getRequestVibrationParamsTimeoutMs() {
+        return mVibrationConfig.getRequestVibrationParamsTimeoutMs();
+    }
+
+    /**
+     * The list of usages that should request vibration params before they are played. These
+     * usages don't have strong latency requirements, e.g. ringtone and notification, and can be
+     * slightly delayed.
+     */
+    public int[] getRequestVibrationParamsForUsages() {
+        return mVibrationConfig.getRequestVibrationParamsForUsages();
+    }
+
+    /**
      * Return a {@link VibrationEffect} that should be played if the device do not support given
      * {@code effectId}.
      *
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 624da80..9cf942e 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,7 +21,9 @@
 import android.os.Build;
 import android.os.CombinedVibration;
 import android.os.IBinder;
+import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.RampSegment;
@@ -38,6 +40,8 @@
 import java.util.List;
 import java.util.PriorityQueue;
 import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Creates and manages a queue of steps for performing a VibrationEffect, as well as coordinating
@@ -65,15 +69,18 @@
     public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
 
     private final DeviceAdapter mDeviceAdapter;
+    private final VibrationScaler mVibrationScaler;
 
     // Not guarded by lock because it's mostly used to read immutable fields by this conductor.
     // This is only modified here at the prepareToStart method which always runs at the vibration
     // thread, to update the adapted effect and report start time.
     private final HalVibration mVibration;
-
     private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
     private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
 
+    @Nullable
+    private final CompletableFuture<Void> mRequestVibrationParamsFuture;
+
     // Signalling fields.
     // Note that vibrator callback signals may happen inside vibrator HAL calls made by the
     // VibrationThread, or on an external executor, so this lock should not be held for anything
@@ -97,10 +104,14 @@
 
     VibrationStepConductor(HalVibration vib, VibrationSettings vibrationSettings,
             DeviceAdapter deviceAdapter,
+            VibrationScaler vibrationScaler,
+            CompletableFuture<Void> requestVibrationParamsFuture,
             VibrationThread.VibratorManagerHooks vibratorManagerHooks) {
         this.mVibration = vib;
         this.vibrationSettings = vibrationSettings;
         this.mDeviceAdapter = deviceAdapter;
+        mVibrationScaler = vibrationScaler;
+        mRequestVibrationParamsFuture = requestVibrationParamsFuture;
         this.vibratorManagerHooks = vibratorManagerHooks;
         this.mSignalVibratorsComplete =
                 new IntArray(mDeviceAdapter.getAvailableVibratorIds().length);
@@ -143,7 +154,15 @@
         if (Build.IS_DEBUGGABLE) {
             expectIsVibrationThread(true);
         }
-        // Scaling happened before the effect was dispatched to this conductor (or to input devices)
+
+        if (!mVibration.callerInfo.attrs.isFlagSet(
+                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
+            if (Flags.adaptiveHapticsEnabled()) {
+                waitForVibrationParamsIfRequired();
+            }
+            mVibration.scaleEffects(mVibrationScaler::scale);
+        }
+
         mVibration.adaptToDevice(mDeviceAdapter);
         CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
         mPendingVibrateSteps++;
@@ -361,6 +380,9 @@
                             + mSignalCancelImmediate);
                 }
             }
+            if (mRequestVibrationParamsFuture != null) {
+                mRequestVibrationParamsFuture.cancel(/* mayInterruptIfRunning= */true);
+            }
             mLock.notify();
         }
     }
@@ -420,6 +442,30 @@
         }
     }
 
+    /**
+     * Blocks until the vibration params future is complete.
+     *
+     * This should be called by the VibrationThread and may be interrupted by calling
+     * `notifyCancelled` from outside it.
+     */
+    private void waitForVibrationParamsIfRequired() {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(true);
+        }
+
+        if (mRequestVibrationParamsFuture == null) {
+            return;
+        }
+
+        try {
+            mRequestVibrationParamsFuture.orTimeout(
+                    vibrationSettings.getRequestVibrationParamsTimeoutMs(),
+                    TimeUnit.MILLISECONDS).get();
+        } catch (Throwable e) {
+            Slog.w(TAG, "Failed to retrieve vibration params.", e);
+        }
+    }
+
     @GuardedBy("mLock")
     private boolean hasPendingNotifySignalLocked() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorControlService.java b/services/core/java/com/android/server/vibrator/VibratorControlService.java
index 9d75249..8f8fe3c 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControlService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControlService.java
@@ -16,11 +16,13 @@
 
 package com.android.server.vibrator;
 
+import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY;
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
+import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
 import static android.os.VibrationAttributes.USAGE_RINGTONE;
 import static android.os.VibrationAttributes.USAGE_TOUCH;
 import static android.os.VibrationAttributes.USAGE_UNKNOWN;
@@ -32,12 +34,18 @@
 import android.frameworks.vibrator.IVibratorController;
 import android.frameworks.vibrator.ScaleParam;
 import android.frameworks.vibrator.VibrationParam;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.VibrationAttributes;
 import android.util.Slog;
-import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * Implementation of {@link IVibratorControlService} which allows the registration of
@@ -47,16 +55,25 @@
  */
 public final class VibratorControlService extends IVibratorControlService.Stub {
     private static final String TAG = "VibratorControlService";
+    private static final int UNRECOGNIZED_VIBRATION_TYPE = -1;
+    private static final int NO_SCALE = -1;
 
     private final VibratorControllerHolder mVibratorControllerHolder;
     private final VibrationScaler mVibrationScaler;
     private final Object mLock;
+    private final int[] mRequestVibrationParamsForUsages;
+
+    @GuardedBy("mLock")
+    private CompletableFuture<Void> mRequestVibrationParamsFuture = null;
+    @GuardedBy("mLock")
+    private IBinder mRequestVibrationParamsToken;
 
     public VibratorControlService(VibratorControllerHolder vibratorControllerHolder,
-            VibrationScaler vibrationScaler, Object lock) {
+            VibrationScaler vibrationScaler, VibrationSettings vibrationSettings, Object lock) {
         mVibratorControllerHolder = vibratorControllerHolder;
         mVibrationScaler = vibrationScaler;
         mLock = lock;
+        mRequestVibrationParamsForUsages = vibrationSettings.getRequestVibrationParamsForUsages();
     }
 
     @Override
@@ -85,8 +102,9 @@
                         + "controller doesn't match the registered one. " + this);
                 return;
             }
-            updateAdaptiveHapticsScales(/* params= */ null);
+            mVibrationScaler.clearAdaptiveHapticsScales();
             mVibratorControllerHolder.setVibratorController(null);
+            endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
         }
     }
 
@@ -131,10 +149,8 @@
                         + "controller doesn't match the registered one. " + this);
                 return;
             }
-            //TODO(305942827): Update this method to only clear the specified vibration types.
-            // Perhaps look into whether it makes more sense to have this clear all scales and
-            // rely on setVibrationParams for clearing the scales for specific vibrations.
-            updateAdaptiveHapticsScales(/* params= */ null);
+
+            updateAdaptiveHapticsScales(types, NO_SCALE);
         }
     }
 
@@ -142,7 +158,26 @@
     public void onRequestVibrationParamsComplete(
             @NonNull IBinder requestToken, @SuppressLint("ArrayReturn") VibrationParam[] result)
             throws RemoteException {
-        // TODO(305942827): Cache the vibration params in VibrationScaler
+        Objects.requireNonNull(requestToken);
+
+        synchronized (mLock) {
+            if (mRequestVibrationParamsToken == null) {
+                Slog.wtf(TAG,
+                        "New vibration params received but no token was cached in the service. "
+                                + "New vibration params ignored.");
+                return;
+            }
+
+            if (!Objects.equals(requestToken, mRequestVibrationParamsToken)) {
+                Slog.w(TAG,
+                        "New vibration params received but the provided token does not match the "
+                                + "cached one. New vibration params ignored.");
+                return;
+            }
+
+            updateAdaptiveHapticsScales(result);
+            endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ false);
+        }
     }
 
     @Override
@@ -156,50 +191,190 @@
     }
 
     /**
-     * Extracts the vibration scales and caches them in {@link VibrationScaler}.
+     * If an {@link IVibratorController} is registered to the service, it will request the latest
+     * vibration params and return a {@link CompletableFuture} that completes when the request is
+     * fulfilled. Otherwise, ignores the call and returns null.
      *
-     * @param params the new vibration params to cache.
+     * @param usage a {@link android.os.VibrationAttributes} usage.
+     * @param timeoutInMillis the request's timeout in millis.
+     * @return a {@link CompletableFuture} to track the completion of the vibration param
+     * request, or null if no {@link IVibratorController} is registered.
      */
-    private void updateAdaptiveHapticsScales(@Nullable VibrationParam[] params) {
-        if (params == null || params.length == 0) {
-            mVibrationScaler.updateAdaptiveHapticsScales(null);
-            return;
-        }
+    @Nullable
+    public CompletableFuture<Void> triggerVibrationParamsRequest(
+            @VibrationAttributes.Usage int usage, int timeoutInMillis) {
+        synchronized (mLock) {
+            IVibratorController vibratorController =
+                    mVibratorControllerHolder.getVibratorController();
+            if (vibratorController == null) {
+                Slog.d(TAG, "Unable to request vibration params. There is no registered "
+                        + "IVibrationController.");
+                return null;
+            }
 
-        SparseArray<Float> vibrationScales = new SparseArray<>();
-        for (int i = 0; i < params.length; i++) {
-            ScaleParam scaleParam = params[i].getScale();
-            extractVibrationScales(scaleParam, vibrationScales);
+            int vibrationType = mapToAdaptiveVibrationType(usage);
+            if (vibrationType == UNRECOGNIZED_VIBRATION_TYPE) {
+                Slog.d(TAG, "Unable to request vibration params. The provided usage " + usage
+                        + " is unrecognized.");
+                return null;
+            }
+
+            try {
+                endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
+                mRequestVibrationParamsFuture = new CompletableFuture<>();
+                mRequestVibrationParamsToken = new Binder();
+                vibratorController.requestVibrationParams(vibrationType, timeoutInMillis,
+                        mRequestVibrationParamsToken);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to request vibration params.", e);
+                endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
+            }
+
+            return mRequestVibrationParamsFuture;
         }
-        mVibrationScaler.updateAdaptiveHapticsScales(vibrationScales);
     }
 
     /**
-     * Extracts the vibration scales and map them to their corresponding
-     * {@link android.os.VibrationAttributes} usages.
+     * If an {@link IVibratorController} is registered to the service, then it checks whether to
+     * request new vibration params before playing the vibration. Returns true if the
+     * usage is for high latency vibrations, e.g. ringtone and notification, and can be delayed
+     * slightly. Otherwise, returns false.
+     *
+     * @param usage a {@link android.os.VibrationAttributes} usage.
+     * @return true if usage is for high latency vibrations, false otherwise.
      */
-    private void extractVibrationScales(ScaleParam scaleParam, SparseArray<Float> vibrationScales) {
-        if ((ScaleParam.TYPE_ALARM & scaleParam.typesMask) != 0) {
-            vibrationScales.put(USAGE_ALARM, scaleParam.scale);
+    public boolean shouldRequestVibrationParams(@VibrationAttributes.Usage int usage) {
+        synchronized (mLock) {
+            IVibratorController vibratorController =
+                    mVibratorControllerHolder.getVibratorController();
+            if (vibratorController == null) {
+                Slog.d(TAG, "Unable to check if should request vibration params. "
+                        + "There is no registered IVibrationController.");
+                return false;
+            }
+
+            return ArrayUtils.contains(mRequestVibrationParamsForUsages, usage);
+        }
+    }
+
+    /**
+     * Returns the {@link #mRequestVibrationParamsToken} which is used to validate
+     * {@link #onRequestVibrationParamsComplete(IBinder, VibrationParam[])} calls.
+     */
+    @VisibleForTesting
+    public IBinder getRequestVibrationParamsToken() {
+        synchronized (mLock) {
+            return mRequestVibrationParamsToken;
+        }
+    }
+
+    /**
+     * Completes or cancels the vibration params request future and resets the future and token
+     * to null.
+     * @param wasCancelled specifies whether the future should be ended by being cancelled or not.
+     */
+    @GuardedBy("mLock")
+    private void endOngoingRequestVibrationParamsLocked(boolean wasCancelled) {
+        mRequestVibrationParamsToken = null;
+        if (mRequestVibrationParamsFuture == null) {
+            return;
         }
 
-        if ((ScaleParam.TYPE_NOTIFICATION & scaleParam.typesMask) != 0) {
-            vibrationScales.put(USAGE_NOTIFICATION, scaleParam.scale);
-            vibrationScales.put(USAGE_COMMUNICATION_REQUEST, scaleParam.scale);
+        if (wasCancelled) {
+            mRequestVibrationParamsFuture.cancel(/* mayInterruptIfRunning= */ true);
+        } else {
+            mRequestVibrationParamsFuture.complete(null);
         }
 
-        if ((ScaleParam.TYPE_RINGTONE & scaleParam.typesMask) != 0) {
-            vibrationScales.put(USAGE_RINGTONE, scaleParam.scale);
+        mRequestVibrationParamsFuture = null;
+    }
+
+    private static int mapToAdaptiveVibrationType(@VibrationAttributes.Usage int usage) {
+        switch (usage) {
+            case USAGE_ALARM -> {
+                return ScaleParam.TYPE_ALARM;
+            }
+            case USAGE_NOTIFICATION, USAGE_COMMUNICATION_REQUEST -> {
+                return ScaleParam.TYPE_NOTIFICATION;
+            }
+            case USAGE_RINGTONE -> {
+                return ScaleParam.TYPE_RINGTONE;
+            }
+            case USAGE_MEDIA, USAGE_UNKNOWN -> {
+                return ScaleParam.TYPE_MEDIA;
+            }
+            case USAGE_TOUCH, USAGE_HARDWARE_FEEDBACK, USAGE_ACCESSIBILITY,
+                    USAGE_PHYSICAL_EMULATION -> {
+                return ScaleParam.TYPE_INTERACTIVE;
+            }
+            default -> {
+                Slog.w(TAG, "Unrecognized vibration usage " + usage);
+                return UNRECOGNIZED_VIBRATION_TYPE;
+            }
+        }
+    }
+
+    /**
+     * Updates the adaptive haptics scales cached in {@link VibrationScaler} with the
+     * provided params.
+     *
+     * @param params the new vibration params.
+     */
+    private void updateAdaptiveHapticsScales(@Nullable VibrationParam[] params) {
+        for (VibrationParam param : params) {
+            ScaleParam scaleParam = param.getScale();
+            updateAdaptiveHapticsScales(scaleParam.typesMask, scaleParam.scale);
+        }
+    }
+
+    /**
+     * Updates the adaptive haptics scales, cached in {@link VibrationScaler}, for the provided
+     * vibration types.
+     *
+     * @param types The type of vibrations.
+     * @param scale The scaling factor that should be applied to the vibrations.
+     */
+    private void updateAdaptiveHapticsScales(int types, float scale) {
+        if ((ScaleParam.TYPE_ALARM & types) != 0) {
+            updateOrRemoveAdaptiveHapticsScale(USAGE_ALARM, scale);
         }
 
-        if ((ScaleParam.TYPE_MEDIA & scaleParam.typesMask) != 0) {
-            vibrationScales.put(USAGE_MEDIA, scaleParam.scale);
-            vibrationScales.put(USAGE_UNKNOWN, scaleParam.scale);
+        if ((ScaleParam.TYPE_NOTIFICATION & types) != 0) {
+            updateOrRemoveAdaptiveHapticsScale(USAGE_NOTIFICATION, scale);
+            updateOrRemoveAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, scale);
         }
 
-        if ((ScaleParam.TYPE_INTERACTIVE & scaleParam.typesMask) != 0) {
-            vibrationScales.put(USAGE_TOUCH, scaleParam.scale);
-            vibrationScales.put(USAGE_HARDWARE_FEEDBACK, scaleParam.scale);
+        if ((ScaleParam.TYPE_RINGTONE & types) != 0) {
+            updateOrRemoveAdaptiveHapticsScale(USAGE_RINGTONE, scale);
         }
+
+        if ((ScaleParam.TYPE_MEDIA & types) != 0) {
+            updateOrRemoveAdaptiveHapticsScale(USAGE_MEDIA, scale);
+            updateOrRemoveAdaptiveHapticsScale(USAGE_UNKNOWN, scale);
+        }
+
+        if ((ScaleParam.TYPE_INTERACTIVE & types) != 0) {
+            updateOrRemoveAdaptiveHapticsScale(USAGE_TOUCH, scale);
+            updateOrRemoveAdaptiveHapticsScale(USAGE_HARDWARE_FEEDBACK, scale);
+        }
+    }
+
+    /**
+     * Updates or removes the adaptive haptics scale for the specified usage. If the scale is set
+     * to {@link #NO_SCALE} then it will be removed from the cached usage scales in
+     * {@link VibrationScaler}. Otherwise, the cached usage scale will be updated by the new value.
+     *
+     * @param usageHint one of VibrationAttributes.USAGE_*.
+     * @param scale     The scaling factor that should be applied to the vibrations. If set to
+     *                  {@link #NO_SCALE} then the scale will be removed.
+     */
+    private void updateOrRemoveAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint,
+            float scale) {
+        if (scale == NO_SCALE) {
+            mVibrationScaler.removeAdaptiveHapticsScale(usageHint);
+            return;
+        }
+
+        mVibrationScaler.updateAdaptiveHapticsScale(usageHint, scale);
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java b/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
index 63e69db..79a99b3 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
@@ -57,7 +57,7 @@
 
     @Override
     public void binderDied(@NonNull IBinder deadBinder) {
-        if (deadBinder == mVibratorController.asBinder()) {
+        if (mVibratorController != null && deadBinder == mVibratorController.asBinder()) {
             setVibratorController(null);
         }
     }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 2c1ab95..759450b 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -53,6 +53,7 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.VibratorInfo;
+import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.VibrationEffectSegment;
 import android.os.vibrator.VibratorInfoFactory;
@@ -84,6 +85,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -161,6 +163,7 @@
 
     private final VibrationSettings mVibrationSettings;
     private final VibrationScaler mVibrationScaler;
+    private final VibratorControlService mVibratorControlService;
     private final InputDeviceDelegate mInputDeviceDelegate;
     private final DeviceAdapter mDeviceAdapter;
 
@@ -212,6 +215,9 @@
 
         mVibrationSettings = new VibrationSettings(mContext, mHandler);
         mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings);
+        mVibratorControlService = new VibratorControlService(
+                injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings,
+                mLock);
         mInputDeviceDelegate = new InputDeviceDelegate(mContext, mHandler);
 
         VibrationCompleteListener listener = new VibrationCompleteListener(this);
@@ -272,9 +278,7 @@
 
         injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
         if (ServiceManager.isDeclared(VIBRATOR_CONTROL_SERVICE)) {
-            injector.addService(VIBRATOR_CONTROL_SERVICE,
-                    new VibratorControlService(new VibratorControllerHolder(), mVibrationScaler,
-                            mLock));
+            injector.addService(VIBRATOR_CONTROL_SERVICE, mVibratorControlService);
         }
 
     }
@@ -783,19 +787,12 @@
     private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
         try {
-            if (!vib.callerInfo.attrs.isFlagSet(
-                    VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
-                // Scale effect before dispatching it to the input devices or the vibration thread.
-                vib.scaleEffects(mVibrationScaler::scale);
-            }
-            boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
-                    vib.callerInfo, vib.getEffectToPlay());
-            if (inputDevicesAvailable) {
-                return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+            if (mInputDeviceDelegate.isAvailable()) {
+                return startVibrationOnInputDevicesLocked(vib);
             }
 
-            VibrationStepConductor conductor = new VibrationStepConductor(vib, mVibrationSettings,
-                    mDeviceAdapter, mVibrationThreadCallbacks);
+            VibrationStepConductor conductor = createVibrationStepConductor(vib);
+
             if (mCurrentVibration == null) {
                 return startVibrationOnThreadLocked(conductor);
             }
@@ -866,6 +863,34 @@
                 vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
     }
 
+    private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
+        CompletableFuture<Void> requestVibrationParamsFuture = null;
+
+        if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet(
+                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
+                && mVibratorControlService.shouldRequestVibrationParams(
+                vib.callerInfo.attrs.getUsage())) {
+            requestVibrationParamsFuture =
+                    mVibratorControlService.triggerVibrationParamsRequest(
+                            vib.callerInfo.attrs.getUsage(),
+                            mVibrationSettings.getRequestVibrationParamsTimeoutMs());
+        }
+
+        return new VibrationStepConductor(vib, mVibrationSettings, mDeviceAdapter, mVibrationScaler,
+                requestVibrationParamsFuture, mVibrationThreadCallbacks);
+    }
+
+    private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
+        if (!vib.callerInfo.attrs.isFlagSet(
+                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
+            // Scale effect before dispatching it to the input devices.
+            vib.scaleEffects(mVibrationScaler::scale);
+        }
+        mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
+
+        return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+    }
+
     private void logVibrationStatus(int uid, VibrationAttributes attrs,
             Vibration.Status status) {
         switch (status) {
@@ -1395,6 +1420,10 @@
         void addService(String name, IBinder service) {
             ServiceManager.addService(name, service);
         }
+
+        VibratorControllerHolder createVibratorControllerHolder() {
+            return new VibratorControllerHolder();
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index fa8c35a..19ea9f9 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -626,6 +626,8 @@
 
         private boolean mForceShowMagnifiableBounds = false;
 
+        private final MagnificationSpec mMagnificationSpec = new MagnificationSpec();
+
         DisplayMagnifier(WindowManagerService windowManagerService,
                 DisplayContent displayContent,
                 Display display,
@@ -655,13 +657,28 @@
                 mAccessibilityTracing.logTrace(LOG_TAG + ".setMagnificationSpec",
                         FLAGS_MAGNIFICATION_CALLBACK, "spec={" + spec + "}");
             }
-            mMagnifedViewport.updateMagnificationSpec(spec);
+            updateMagnificationSpec(spec);
             mMagnifedViewport.recomputeBounds();
 
             mService.applyMagnificationSpecLocked(mDisplay.getDisplayId(), spec);
             mService.scheduleAnimationLocked();
         }
 
+        void updateMagnificationSpec(MagnificationSpec spec) {
+            if (spec != null) {
+                mMagnificationSpec.initialize(spec.scale, spec.offsetX, spec.offsetY);
+            } else {
+                mMagnificationSpec.clear();
+            }
+            // If this message is pending we are in a rotation animation and do not want
+            // to show the border. We will do so when the pending message is handled.
+            if (!mHandler.hasMessages(
+                    MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
+                mMagnifedViewport.setMagnifiedRegionBorderShown(
+                        isForceShowingMagnifiableBounds(), true);
+            }
+        }
+
         void setForceShowMagnifiableBounds(boolean show) {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
                 mAccessibilityTracing.logTrace(LOG_TAG + ".setForceShowMagnifiableBounds",
@@ -800,8 +817,7 @@
                         case WindowManager.LayoutParams.TYPE_QS_DIALOG:
                         case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: {
                             Rect magnifiedRegionBounds = mTempRect2;
-                            mMagnifedViewport.getMagnifiedFrameInContentCoords(
-                                    magnifiedRegionBounds);
+                            getMagnifiedFrameInContentCoords(magnifiedRegionBounds);
                             Rect touchableRegionBounds = mTempRect1;
                             windowState.getTouchableRegion(mTempRegion1);
                             mTempRegion1.getBounds(touchableRegionBounds);
@@ -818,6 +834,14 @@
             }
         }
 
+        void getMagnifiedFrameInContentCoords(Rect rect) {
+            Region magnificationRegion = new Region();
+            mMagnifedViewport.getMagnificationRegion(magnificationRegion);
+            magnificationRegion.getBounds(rect);
+            rect.offset((int) -mMagnificationSpec.offsetX, (int) -mMagnificationSpec.offsetY);
+            rect.scale(1.0f / mMagnificationSpec.scale);
+        }
+
         void notifyImeWindowVisibilityChanged(boolean shown) {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
                 mAccessibilityTracing.logTrace(LOG_TAG + ".notifyImeWindowVisibilityChanged",
@@ -832,13 +856,13 @@
                 mAccessibilityTracing.logTrace(LOG_TAG + ".getMagnificationSpecForWindow",
                         FLAGS_MAGNIFICATION_CALLBACK, "windowState={" + windowState + "}");
             }
-            MagnificationSpec spec = mMagnifedViewport.getMagnificationSpec();
-            if (spec != null && !spec.isNop()) {
+
+            if (mMagnificationSpec != null && !mMagnificationSpec.isNop()) {
                 if (!windowState.shouldMagnify()) {
                     return null;
                 }
             }
-            return spec;
+            return mMagnificationSpec;
         }
 
         void getMagnificationRegion(Region outMagnificationRegion) {
@@ -852,6 +876,10 @@
             mMagnifedViewport.getMagnificationRegion(outMagnificationRegion);
         }
 
+        boolean isMagnifying() {
+            return mMagnificationSpec.scale > 1.0f;
+        }
+
         void destroy() {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
                 mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK);
@@ -897,8 +925,6 @@
 
             private final Path mCircularPath;
 
-            private final MagnificationSpec mMagnificationSpec = new MagnificationSpec();
-
             private final float mBorderWidth;
             private final int mHalfBorderWidth;
             private final int mDrawBorderInset;
@@ -932,20 +958,6 @@
                 outMagnificationRegion.set(mMagnificationRegion);
             }
 
-            void updateMagnificationSpec(MagnificationSpec spec) {
-                if (spec != null) {
-                    mMagnificationSpec.initialize(spec.scale, spec.offsetX, spec.offsetY);
-                } else {
-                    mMagnificationSpec.clear();
-                }
-                // If this message is pending we are in a rotation animation and do not want
-                // to show the border. We will do so when the pending message is handled.
-                if (!mHandler.hasMessages(
-                        MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
-                    setMagnifiedRegionBorderShown(isForceShowingMagnifiableBounds(), true);
-                }
-            }
-
             void recomputeBounds() {
                 getDisplaySizeLocked(mScreenSize);
                 final int screenWidth = mScreenSize.x;
@@ -1127,21 +1139,6 @@
                 }
             }
 
-            void getMagnifiedFrameInContentCoords(Rect rect) {
-                MagnificationSpec spec = mMagnificationSpec;
-                mMagnificationRegion.getBounds(rect);
-                rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
-                rect.scale(1.0f / spec.scale);
-            }
-
-            boolean isMagnifying() {
-                return mMagnificationSpec.scale > 1.0f;
-            }
-
-            MagnificationSpec getMagnificationSpec() {
-                return mMagnificationSpec;
-            }
-
             void drawWindowIfNeeded() {
                 recomputeBounds();
                 mWindow.postDrawIfNeeded();
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 2e0546e..1128d0c 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1319,9 +1319,33 @@
         try {
             synchronized (mGlobalLock) {
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-                if (r != null) {
+                if (r == null) return;
+                final TransitionController controller = r.mTransitionController;
+                if (!controller.isShellTransitionsEnabled()) {
                     r.setShowWhenLocked(showWhenLocked);
+                    return;
                 }
+                if (controller.isCollecting()
+                        && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) {
+                    // Keyguard isn't locked, so this can be done as part of the collecting
+                    // transition.
+                    r.setShowWhenLocked(showWhenLocked);
+                    return;
+                }
+                final Transition transition = new Transition(
+                        showWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK,
+                        0 /* flags */, controller, mService.mWindowManager.mSyncEngine);
+                r.mTransitionController.startCollectOrQueue(transition, (deferred) -> {
+                    transition.collect(r);
+                    r.setShowWhenLocked(showWhenLocked);
+                    if (transition.isNoOp()) {
+                        transition.abort();
+                        return;
+                    }
+                    controller.requestStartTransition(transition, null /* trigger */,
+                            null /* remoteTransition */, null /* displayChange */);
+                    transition.setReady(r, true);
+                });
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -1334,9 +1358,34 @@
         try {
             synchronized (mGlobalLock) {
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-                if (r != null) {
+                if (r == null) return;
+                final TransitionController controller = r.mTransitionController;
+                // If shell transitions is not enabled just set it directly.
+                if (!controller.isShellTransitionsEnabled()) {
                     r.setInheritShowWhenLocked(inheritShowWhenLocked);
+                    return;
                 }
+                if (controller.isCollecting()
+                        && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) {
+                    // Keyguard isn't locked, so this can be done as part of the collecting
+                    // transition.
+                    r.setInheritShowWhenLocked(inheritShowWhenLocked);
+                    return;
+                }
+                final Transition transition = new Transition(
+                        inheritShowWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK,
+                        0 /* flags */, controller, mService.mWindowManager.mSyncEngine);
+                r.mTransitionController.startCollectOrQueue(transition, (deferred) -> {
+                    transition.collect(r);
+                    r.setInheritShowWhenLocked(inheritShowWhenLocked);
+                    if (transition.isNoOp()) {
+                        transition.abort();
+                        return;
+                    }
+                    controller.requestStartTransition(transition, null /* trigger */,
+                            null /* remoteTransition */, null /* displayChange */);
+                    transition.setReady(r, true);
+                });
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 03d55d9..b1d04c9 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -545,9 +545,6 @@
     boolean launchFailed;   // set if a launched failed, to abort on 2nd try
     boolean delayedResume;  // not yet resumed because of stopped app switches?
     boolean finishing;      // activity in pending finish list?
-    boolean deferRelaunchUntilPaused;   // relaunch of activity is being deferred until pause is
-                                        // completed
-    boolean preserveWindowOnDeferredRelaunch; // activity windows are preserved on deferred relaunch
     int configChangeFlags;  // which config values have changed
     private boolean keysPaused;     // has key dispatching been paused for it?
     int launchMode;         // the launch mode activity attribute.
@@ -1277,10 +1274,8 @@
         if (mDeferHidingClient) {
             pw.println(prefix + "mDeferHidingClient=" + mDeferHidingClient);
         }
-        if (deferRelaunchUntilPaused || configChangeFlags != 0) {
-            pw.print(prefix); pw.print("deferRelaunchUntilPaused=");
-                    pw.print(deferRelaunchUntilPaused);
-                    pw.print(" configChangeFlags=");
+        if (configChangeFlags != 0) {
+            pw.print(prefix); pw.print(" configChangeFlags=");
                     pw.println(Integer.toHexString(configChangeFlags));
         }
         if (mServiceConnectionsHolder != null) {
@@ -2137,7 +2132,6 @@
         launchFailed = false;
         delayedResume = false;
         finishing = false;
-        deferRelaunchUntilPaused = false;
         keysPaused = false;
         inHistory = false;
         nowVisible = false;
@@ -4096,8 +4090,6 @@
         // Clean up the splash screen if it was still displayed.
         cleanUpSplashScreen();
 
-        deferRelaunchUntilPaused = false;
-
         if (setState) {
             setState(DESTROYED, "cleanUp");
             if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during cleanUp for activity " + this);
@@ -6481,9 +6473,6 @@
             mAppStopped = true;
             ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this);
             setState(STOPPED, "stopIfPossible");
-            if (deferRelaunchUntilPaused) {
-                destroyImmediately("stop-except");
-            }
         }
     }
 
@@ -6539,12 +6528,7 @@
         if (finishing) {
             abortAndClearOptionsAnimation();
         } else {
-            if (deferRelaunchUntilPaused) {
-                destroyImmediately("stop-config");
-                mRootWindowContainer.resumeFocusedTasksTopActivities();
-            } else {
-                mAtmService.updatePreviousProcess(this);
-            }
+            mAtmService.updatePreviousProcess(this);
         }
         mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
     }
@@ -9724,23 +9708,12 @@
             } else {
                 mRelaunchReason = RELAUNCH_REASON_NONE;
             }
-            if (mState == PAUSING) {
-                // A little annoying: we are waiting for this activity to finish pausing. Let's not
-                // do anything now, but just flag that it needs to be restarted when done pausing.
-                ProtoLog.v(WM_DEBUG_CONFIGURATION,
-                        "Config is skipping already pausing %s", this);
-                deferRelaunchUntilPaused = true;
-                preserveWindowOnDeferredRelaunch = preserveWindow;
-                return true;
-            } else {
-                ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s",
-                        this);
-                if (!mVisibleRequested) {
-                    ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
-                            + "activity %s called by %s", this, Debug.getCallers(4));
-                }
-                relaunchActivityLocked(preserveWindow);
+            ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this);
+            if (!mVisibleRequested) {
+                ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+                        + "activity %s called by %s", this, Debug.getCallers(4));
             }
+            relaunchActivityLocked(preserveWindow);
 
             // All done...  tell the caller we weren't able to keep this activity around.
             return false;
@@ -9958,8 +9931,6 @@
         mTaskSupervisor.mStoppingActivities.remove(this);
 
         configChangeFlags = 0;
-        deferRelaunchUntilPaused = false;
-        preserveWindowOnDeferredRelaunch = false;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 83ccbdc..13f6a5f 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1527,6 +1527,12 @@
                         setLaunchBehind(visibleOpenActivities[i]);
                     }
                 }
+                // Force update mLastSurfaceShowing for opening activity and its task.
+                if (mWindowManagerService.mRoot.mTransitionController.isShellTransitionsEnabled()) {
+                    for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+                        WindowContainer.enforceSurfaceVisible(visibleOpenActivities[i]);
+                    }
+                }
             }
 
             @Nullable Runnable build() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e2bc59b..a7bbc25 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1913,12 +1913,9 @@
              */
             final Rect mConfigFrame = new Rect();
 
-            /** The count of insets sources when calculating this info. */
-            int mLastInsetsSourceCount;
-
             private boolean mNeedUpdate = true;
 
-            void update(DisplayContent dc, int rotation, int w, int h) {
+            InsetsState update(DisplayContent dc, int rotation, int w, int h) {
                 final DisplayFrames df = new DisplayFrames();
                 dc.updateDisplayFrames(df, rotation, w, h);
                 dc.getDisplayPolicy().simulateLayoutDisplay(df);
@@ -1935,8 +1932,8 @@
                 mNonDecorFrame.inset(mNonDecorInsets);
                 mConfigFrame.set(displayFrame);
                 mConfigFrame.inset(mConfigInsets);
-                mLastInsetsSourceCount = dc.getDisplayPolicy().mInsetsSourceWindowsExceptIme.size();
                 mNeedUpdate = false;
+                return insetsState;
             }
 
             void set(Info other) {
@@ -1944,7 +1941,6 @@
                 mConfigInsets.set(other.mConfigInsets);
                 mNonDecorFrame.set(other.mNonDecorFrame);
                 mConfigFrame.set(other.mConfigFrame);
-                mLastInsetsSourceCount = other.mLastInsetsSourceCount;
                 mNeedUpdate = false;
             }
 
@@ -1997,6 +1993,29 @@
             }
         }
 
+        static boolean hasInsetsFrameDiff(InsetsState s1, InsetsState s2, int insetsTypes) {
+            int insetsCount1 = 0;
+            for (int i = s1.sourceSize() - 1; i >= 0; i--) {
+                final InsetsSource source1 = s1.sourceAt(i);
+                if ((source1.getType() & insetsTypes) == 0) {
+                    continue;
+                }
+                insetsCount1++;
+                final InsetsSource source2 = s2.peekSource(source1.getId());
+                if (source2 == null || !source2.getFrame().equals(source1.getFrame())) {
+                    return true;
+                }
+            }
+            int insetsCount2 = 0;
+            for (int i = s2.sourceSize() - 1; i >= 0; i--) {
+                final InsetsSource source2 = s2.sourceAt(i);
+                if ((source2.getType() & insetsTypes) != 0) {
+                    insetsCount2++;
+                }
+            }
+            return insetsCount1 != insetsCount2;
+        }
+
         private static class Cache {
             /**
              * If {@link #mPreserveId} is this value, it is in the middle of updating display
@@ -2031,12 +2050,14 @@
         final int dw = displayFrames.mWidth;
         final int dh = displayFrames.mHeight;
         final DecorInsets.Info newInfo = mDecorInsets.mTmpInfo;
-        newInfo.update(mDisplayContent, rotation, dw, dh);
+        final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh);
         final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh);
         if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) {
             // Even if the config frame is not changed in current rotation, it may change the
-            // insets in other rotations if the source count is changed.
-            if (newInfo.mLastInsetsSourceCount != currentInfo.mLastInsetsSourceCount) {
+            // insets in other rotations if the frame of insets source is changed.
+            final InsetsState currentInsetsState = mDisplayContent.mDisplayFrames.mInsetsState;
+            if (DecorInsets.hasInsetsFrameDiff(
+                    newInsetsState, currentInsetsState, mService.mConfigTypes)) {
                 for (int i = mDecorInsets.mInfoForRotation.length - 1; i >= 0; i--) {
                     if (i != rotation) {
                         final boolean flipSize = (i + rotation) % 2 == 1;
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index 7b23004..7a3124d 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -160,7 +160,8 @@
 
         // Invisible activity should be stopped. If the recents activity is alive and its doesn't
         // need to relaunch by current configuration, then it may be already in stopped state.
-        if (!targetActivity.isState(STOPPING, STOPPED)) {
+        if (!targetActivity.finishing && targetActivity.isAttached()
+                && !targetActivity.isState(STOPPING, STOPPED)) {
             // Add to stopping instead of stop immediately. So the client has the chance to perform
             // traversal in non-stopped state (ViewRootImpl.mStopped) that would initialize more
             // things (e.g. the measure can be done earlier). The actual stop will be performed when
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 11e7bb0..838ce86 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1915,11 +1915,7 @@
                 ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
                                 + "wasStopping=%b visibleRequested=%b",  prev,  wasStopping,
                         prev.isVisibleRequested());
-                if (prev.deferRelaunchUntilPaused) {
-                    // Complete the deferred relaunch that was waiting for pause to complete.
-                    ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
-                    prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch);
-                } else if (wasStopping) {
+                if (wasStopping) {
                     // We are also stopping, the stop request must have gone soon after the pause.
                     // We can't clobber it, because the stop confirmation will not be handled.
                     // We don't need to schedule another stop, we only need to let it happen.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2accf9a..1e58306 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2917,6 +2917,26 @@
         Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, cookie);
     }
 
+    /**
+     * Get whether the transition, in its current state, is a no-op. This should be avoided. It is
+     * only here for legacy usages where we can't tell ahead-of-time whether something will
+     * generate a change.
+     */
+    boolean isNoOp() {
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            // This is the same criteria as the rejection logic in calculateTargets
+            final WindowContainer<?> wc = mParticipants.valueAt(i);
+            if (!wc.isAttached()) continue;
+            // The level of transition target should be at least window token.
+            if (wc.asWindowState() != null) continue;
+            final ChangeInfo changeInfo = mChanges.get(wc);
+            // Reject no-ops
+            if (!changeInfo.hasChanged()) continue;
+            return false;
+        }
+        return true;
+    }
+
     @VisibleForTesting
     static class ChangeInfo {
         private static final int FLAG_NONE = 0;
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 25b5630f..70775530 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -994,39 +994,18 @@
         Slog.e(TAG, "Set visible without transition " + wc + " playing=" + isPlaying
                 + " caller=" + caller);
         if (!isPlaying) {
-            enforceSurfaceVisible(wc);
+            WindowContainer.enforceSurfaceVisible(wc);
             return;
         }
         // Update surface visibility after the playing transitions are finished, so the last
         // visibility won't be replaced by the finish transaction of transition.
         mStateValidators.add(() -> {
             if (wc.isVisibleRequested()) {
-                enforceSurfaceVisible(wc);
+                WindowContainer.enforceSurfaceVisible(wc);
             }
         });
     }
 
-    private void enforceSurfaceVisible(WindowContainer<?> wc) {
-        if (wc.mSurfaceControl == null) return;
-        wc.getSyncTransaction().show(wc.mSurfaceControl);
-        final ActivityRecord ar = wc.asActivityRecord();
-        if (ar != null) {
-            ar.mLastSurfaceShowing = true;
-        }
-        // Force showing the parents because they may be hidden by previous transition.
-        for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
-                p = p.getParent()) {
-            if (p.mSurfaceControl != null) {
-                p.getSyncTransaction().show(p.mSurfaceControl);
-                final Task task = p.asTask();
-                if (task != null) {
-                    task.mLastSurfaceShowing = true;
-                }
-            }
-        }
-        wc.scheduleAnimation();
-    }
-
     /**
      * Called when the transition has a complete set of participants for its operation. In other
      * words, it is when the transition is "ready" but is still waiting for participants to draw.
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 286182e..2d2857a 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3625,6 +3625,29 @@
         return mSurfaceControl.getHeight();
     }
 
+    static void enforceSurfaceVisible(@NonNull WindowContainer<?> wc) {
+        if (wc.mSurfaceControl == null) {
+            return;
+        }
+        wc.getSyncTransaction().show(wc.mSurfaceControl);
+        final ActivityRecord ar = wc.asActivityRecord();
+        if (ar != null) {
+            ar.mLastSurfaceShowing = true;
+        }
+        // Force showing the parents because they may be hidden by previous transition.
+        for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
+                p = p.getParent()) {
+            if (p.mSurfaceControl != null) {
+                p.getSyncTransaction().show(p.mSurfaceControl);
+                final Task task = p.asTask();
+                if (task != null) {
+                    task.mLastSurfaceShowing = true;
+                }
+            }
+        }
+        wc.scheduleAnimation();
+    }
+
     @CallSuper
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         if (mSurfaceAnimator.isAnimating()) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3f889c0..8cd399f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1471,12 +1471,11 @@
                         final int index = task.mChildren.indexOf(topTaskFragment);
                         task.mChildren.remove(taskFragment);
                         task.mChildren.add(index, taskFragment);
-                        if (taskFragment.hasChild()) {
-                            effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                        } else {
+                        if (!taskFragment.hasChild()) {
                             // Ensure that the child layers are updated if the TaskFragment is empty
                             task.assignChildLayers();
                         }
+                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
                     }
                 }
                 break;
@@ -1491,12 +1490,11 @@
                 if (task != null) {
                     task.mChildren.remove(taskFragment);
                     task.mChildren.add(0, taskFragment);
-                    if (taskFragment.hasChild()) {
-                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                    } else {
+                    if (!taskFragment.hasChild()) {
                         // Ensure that the child layers are updated if the TaskFragment is empty.
                         task.assignChildLayers();
                     }
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
                 break;
             }
@@ -1505,12 +1503,11 @@
                 if (task != null) {
                     task.mChildren.remove(taskFragment);
                     task.mChildren.add(taskFragment);
-                    if (taskFragment.hasChild()) {
-                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                    } else {
+                    if (!taskFragment.hasChild()) {
                         // Ensure that the child layers are updated if the TaskFragment is empty.
                         task.assignChildLayers();
                     }
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
                 break;
             }
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 7b08413..4403bce 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -571,8 +571,8 @@
 }
 
 static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) {
-    int uid = getuid();
-    int pid = getpid();
+    uid_t uid = getuid();
+    pid_t pid = getpid();
 
     return isProfileValidForProcess("Frozen", uid, pid) &&
             isProfileValidForProcess("Unfrozen", uid, pid);
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index cbc301b..4a6b31c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -290,6 +290,7 @@
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
+    void setTouchpadTapDraggingEnabled(bool enabled);
     void setTouchpadRightClickZoneEnabled(bool enabled);
     void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
     void setShowTouches(bool enabled);
@@ -440,6 +441,9 @@
         // True to enable tap-to-click on touchpads.
         bool touchpadTapToClickEnabled{true};
 
+        // True to enable tap dragging on touchpads.
+        bool touchpadTapDraggingEnabled{false};
+
         // True to enable a zone on the right-hand side of touchpads where clicks will be turned
         // into context (a.k.a. "right") clicks.
         bool touchpadRightClickZoneEnabled{false};
@@ -697,6 +701,7 @@
         outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
         outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
         outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
+        outConfig->touchpadTapDraggingEnabled = mLocked.touchpadTapDraggingEnabled;
         outConfig->touchpadRightClickZoneEnabled = mLocked.touchpadRightClickZoneEnabled;
 
         outConfig->disabledDevices = mLocked.disabledInputDevices;
@@ -1301,6 +1306,22 @@
             InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
+void NativeInputManager::setTouchpadTapDraggingEnabled(bool enabled) {
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        if (mLocked.touchpadTapDraggingEnabled == enabled) {
+            return;
+        }
+
+        ALOGI("Setting touchpad tap dragging to %s.", toString(enabled));
+        mLocked.touchpadTapDraggingEnabled = enabled;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
+}
+
 void NativeInputManager::setTouchpadRightClickZoneEnabled(bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
@@ -2223,6 +2244,13 @@
     im->setTouchpadTapToClickEnabled(enabled);
 }
 
+static void nativeSetTouchpadTapDraggingEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+    im->setTouchpadTapDraggingEnabled(enabled);
+}
+
 static void nativeSetTouchpadRightClickZoneEnabled(JNIEnv* env, jobject nativeImplObj,
                                                    jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2844,6 +2872,7 @@
         {"setTouchpadNaturalScrollingEnabled", "(Z)V",
          (void*)nativeSetTouchpadNaturalScrollingEnabled},
         {"setTouchpadTapToClickEnabled", "(Z)V", (void*)nativeSetTouchpadTapToClickEnabled},
+        {"setTouchpadTapDraggingEnabled", "(Z)V", (void*)nativeSetTouchpadTapDraggingEnabled},
         {"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
         {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
         {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 667e086..281fb1c 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -27,6 +27,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -201,7 +202,7 @@
     @SuppressWarnings("GuardedBy") // ErrorProne requires service.mLock which is the same
     // this.mLock
     protected void handlePackageRemovedMultiModeLocked(String packageName, int userId) {
-        updateProvidersWhenPackageRemoved(mContext, packageName);
+        updateProvidersWhenPackageRemoved(new SettingsWrapper(mContext), packageName);
 
         List<CredentialManagerServiceImpl> services = peekServiceListForUserLocked(userId);
         if (services == null) {
@@ -1134,13 +1135,14 @@
     }
 
     /** Updates the list of providers when an app is uninstalled. */
-    public static void updateProvidersWhenPackageRemoved(Context context, String packageName) {
+    public static void updateProvidersWhenPackageRemoved(
+            SettingsWrapper settingsWrapper, String packageName) {
+        Slog.i(TAG, "updateProvidersWhenPackageRemoved");
+
         // Get the current providers.
         String rawProviders =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, UserHandle.myUserId());
         if (rawProviders == null) {
             Slog.w(TAG, "settings key is null");
             return;
@@ -1148,44 +1150,44 @@
 
         // Remove any providers from the primary setting that contain the package name
         // being removed.
-        Set<String> primaryProviders =
-                getStoredProviders(rawProviders, packageName);
-        if (!Settings.Secure.putString(
-                context.getContentResolver(),
+        Set<String> primaryProviders = getStoredProviders(rawProviders, packageName);
+        if (!settingsWrapper.putStringForUser(
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
-                String.join(":", primaryProviders))) {
-            Slog.w(TAG, "Failed to remove primary package: " + packageName);
+                String.join(":", primaryProviders),
+                UserHandle.myUserId(),
+                /* overrideableByRestore= */ true)) {
+            Slog.e(TAG, "Failed to remove primary package: " + packageName);
             return;
         }
 
         // Read the autofill provider so we don't accidentally erase it.
         String autofillProvider =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.AUTOFILL_SERVICE,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.AUTOFILL_SERVICE, UserHandle.myUserId());
 
         // If there is an autofill provider and it is the placeholder indicating
         // that the currently selected primary provider does not support autofill
         // then we should wipe the setting to keep it in sync.
         if (autofillProvider != null && primaryProviders.isEmpty()) {
             if (autofillProvider.equals(AUTOFILL_PLACEHOLDER_VALUE)) {
-                if (!Settings.Secure.putString(
-                        context.getContentResolver(),
+                if (!settingsWrapper.putStringForUser(
                         Settings.Secure.AUTOFILL_SERVICE,
-                        "")) {
-                    Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+                        "",
+                        UserHandle.myUserId(),
+                        /* overrideableByRestore= */ true)) {
+                    Slog.e(TAG, "Failed to remove autofill package: " + packageName);
                 }
             } else {
                 // If the existing autofill provider is from the app being removed
                 // then erase the autofill service setting.
                 ComponentName cn = ComponentName.unflattenFromString(autofillProvider);
                 if (cn != null && cn.getPackageName().equals(packageName)) {
-                   if (!Settings.Secure.putString(
-                            context.getContentResolver(),
+                    if (!settingsWrapper.putStringForUser(
                             Settings.Secure.AUTOFILL_SERVICE,
-                            "")) {
-                        Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+                            "",
+                            UserHandle.myUserId(),
+                            /* overrideableByRestore= */ true)) {
+                        Slog.e(TAG, "Failed to remove autofill package: " + packageName);
                     }
                 }
             }
@@ -1193,19 +1195,17 @@
 
         // Read the credential providers to remove any reference of the removed app.
         String rawCredentialProviders =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.CREDENTIAL_SERVICE,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.CREDENTIAL_SERVICE, UserHandle.myUserId());
 
         // Remove any providers that belong to the removed app.
-        Set<String> credentialProviders =
-                getStoredProviders(rawCredentialProviders, packageName);
-        if (!Settings.Secure.putString(
-                context.getContentResolver(),
+        Set<String> credentialProviders = getStoredProviders(rawCredentialProviders, packageName);
+        if (!settingsWrapper.putStringForUser(
                 Settings.Secure.CREDENTIAL_SERVICE,
-                String.join(":", credentialProviders))) {
-            Slog.w(TAG, "Failed to remove secondary package: " + packageName);
+                String.join(":", credentialProviders),
+                UserHandle.myUserId(),
+                /* overrideableByRestore= */ true)) {
+            Slog.e(TAG, "Failed to remove secondary package: " + packageName);
         }
     }
 
@@ -1232,4 +1232,38 @@
 
         return providers;
     }
+
+    /** A wrapper class that can be used by tests for intercepting reads/writes. */
+    public static class SettingsWrapper {
+        private final Context mContext;
+
+        public SettingsWrapper(@NonNull Context context) {
+            this.mContext = context;
+        }
+
+        ContentResolver getContentResolver() {
+            return mContext.getContentResolver();
+        }
+
+        /** Retrieves the string value of a system setting */
+        public String getStringForUser(String name, int userHandle) {
+            return Settings.Secure.getStringForUser(getContentResolver(), name, userHandle);
+        }
+
+        /** Updates the string value of a system setting */
+        public boolean putStringForUser(
+                String name,
+                String value,
+                int userHandle,
+                boolean overrideableByRestore) {
+            return Settings.Secure.putStringForUser(
+                    getContentResolver(),
+                    name,
+                    value,
+                    null,
+                    false,
+                    userHandle,
+                    overrideableByRestore);
+        }
+    }
 }
diff --git a/services/incremental/OWNERS b/services/incremental/OWNERS
index 7ebb962..c18a9e5 100644
--- a/services/incremental/OWNERS
+++ b/services/incremental/OWNERS
@@ -1,8 +1,4 @@
 # Bug component: 554432
-include /services/core/java/com/android/server/pm/OWNERS
+include /PACKAGE_MANAGER_OWNERS
 
-alexbuy@google.com
-schfan@google.com
-toddke@google.com
-zyy@google.com
-patb@google.com
+zyy@google.com
\ No newline at end of file
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index 8f464d4..fc2eb26 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -17,6 +17,7 @@
 package com.android.server.permission.access.appop
 
 import android.app.AppOpsManager
+import android.companion.virtual.VirtualDeviceManager
 import android.os.Handler
 import android.os.UserHandle
 import android.util.ArrayMap
@@ -213,7 +214,10 @@
                     val uid = key.first
                     val appOpCode = key.second
 
-                    listener.onUidModeChanged(uid, appOpCode, mode)
+                    listener.onUidModeChanged(uid,
+                        appOpCode,
+                        mode,
+                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)
                 }
             }
 
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index edacda0..15c9b9f 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -20,7 +20,6 @@
 import android.os.Build
 import android.util.Slog
 import com.android.server.permission.access.MutateStateScope
-import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.util.andInv
 import com.android.server.permission.access.util.hasAnyBit
@@ -61,10 +60,11 @@
         if (version <= 12 /*&& SdkLevel.isAtLeastT()*/) {
             Slog.v(
                 LOG_TAG,
-                "Upgrading scoped permissions for package: $packageName" +
+                "Upgrading scoped media and body sensor permissions for package: $packageName" +
                     ", version: $version, user: $userId"
             )
             upgradeAuralVisualMediaPermissions(packageState, userId)
+            upgradeBodySensorPermissions(packageState, userId)
         }
         // TODO Enable isAtLeastU check, when moving subsystem to mainline.
         if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) {
@@ -182,6 +182,50 @@
         }
     }
 
+    private fun MutateStateScope.upgradeBodySensorPermissions(
+        packageState: PackageState,
+        userId: Int
+    ) {
+        if (
+            Manifest.permission.BODY_SENSORS_BACKGROUND !in
+                packageState.androidPackage!!.requestedPermissions
+        ) {
+            return
+        }
+
+        // Should have been granted when first getting exempt as if the perm was just split
+        val appId = packageState.appId
+        val backgroundBodySensorsFlags =
+            with(policy) {
+                getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS_BACKGROUND)
+            }
+        if (backgroundBodySensorsFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)) {
+            return
+        }
+
+        // Add Upgrade Exemption - BODY_SENSORS_BACKGROUND is a restricted permission
+        with(policy) {
+            updatePermissionFlags(
+                appId,
+                userId,
+                Manifest.permission.BODY_SENSORS_BACKGROUND,
+                PermissionFlags.UPGRADE_EXEMPT,
+                PermissionFlags.UPGRADE_EXEMPT,
+            )
+        }
+
+        val bodySensorsFlags =
+            with(policy) { getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS) }
+        val isForegroundBodySensorsGranted = PermissionFlags.isAppOpGranted(bodySensorsFlags)
+        if (isForegroundBodySensorsGranted) {
+            grantRuntimePermission(
+                packageState,
+                userId,
+                Manifest.permission.BODY_SENSORS_BACKGROUND
+            )
+        }
+    }
+
     /** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */
     private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission(
         packageState: PackageState,
diff --git a/services/profcollect/OWNERS b/services/profcollect/OWNERS
index b380e39..be9e61f 100644
--- a/services/profcollect/OWNERS
+++ b/services/profcollect/OWNERS
@@ -1,3 +1 @@
-srhines@google.com
-yabinc@google.com
-yikong@google.com
+include platform/prebuilts/clang/host/linux-x86:/OWNERS
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 582b712..fb0fbe8 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -62,7 +62,7 @@
     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
     private static final String INTENT_UPLOAD_PROFILES =
             "com.android.server.profcollect.UPLOAD_PROFILES";
-    private static final long BG_PROCESS_PERIOD = TimeUnit.HOURS.toMillis(4); // every 4 hours.
+    private static final long BG_PROCESS_INTERVAL = TimeUnit.HOURS.toMillis(4); // every 4 hours.
 
     private IProfCollectd mIProfcollect;
     private static ProfcollectForwardingService sSelfService;
@@ -226,7 +226,7 @@
             js.schedule(new JobInfo.Builder(JOB_IDLE_PROCESS, JOB_SERVICE_NAME)
                     .setRequiresDeviceIdle(true)
                     .setRequiresCharging(true)
-                    .setPeriodic(BG_PROCESS_PERIOD)
+                    .setPeriodic(BG_PROCESS_INTERVAL)
                     .setPriority(JobInfo.PRIORITY_MIN)
                     .build());
         }
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 321d945..d928306 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -46,7 +46,6 @@
         "androidx.test.espresso.core",
         "androidx.test.espresso.contrib",
         "androidx.test.ext.truth",
-        "backup_flags_lib",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
diff --git a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
index 47ae97f..0e85626 100644
--- a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
@@ -44,6 +44,7 @@
 import android.app.IActivityManager;
 import android.app.IUidObserver;
 import android.app.usage.UsageStatsManager;
+import android.companion.virtual.VirtualDeviceManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -309,7 +310,8 @@
             mRestrictedPackages.remove(p);
         }
         if (mAppOpsCallback != null) {
-            mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
+            mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName,
+                    VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
         }
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index b39cd04..a476155 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -125,6 +125,7 @@
 import android.app.compat.CompatChanges;
 import android.app.tare.EconomyManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.companion.virtual.VirtualDeviceManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -2980,7 +2981,8 @@
         mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ALLOWED);
         mockScheduleExactAlarmStatePreT(true, MODE_ERRORED);
 
-        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+                VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
         assertAndHandleMessageSync(REMOVE_EXACT_ALARMS);
         verify(mService).removeExactAlarmsOnPermissionRevoked(TEST_CALLING_UID,
                 TEST_CALLING_PACKAGE, true);
@@ -2993,7 +2995,8 @@
         mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ALLOWED);
         mockScheduleExactAlarmStatePreT(true, MODE_ERRORED);
 
-        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+                VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
 
         verify(mService.mHandler, never()).sendMessageAtTime(
                 argThat(m -> m.what == REMOVE_EXACT_ALARMS), anyLong());
@@ -3008,7 +3011,8 @@
 
         mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ERRORED);
         mockScheduleExactAlarmStatePreT(true, MODE_ALLOWED);
-        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+        mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+                VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
 
         final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index a65ef00..bf00b75 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -552,20 +552,22 @@
         when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
                 null);
 
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isNull();
     }
 
     @Test
     public void getArchivedAppIcon_notArchived() {
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isNull();
     }
 
     @Test
     public void getArchivedAppIcon_success() {
         mUserState.setArchiveState(createArchiveState()).setInstalled(false);
 
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo(
-                mIcon);
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isEqualTo(mIcon);
     }
 
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
index 4dae2d5..8e53d52 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
@@ -28,6 +28,9 @@
 import android.os.BatteryConsumer;
 import android.os.Binder;
 import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -38,6 +41,7 @@
 import com.android.internal.os.KernelSingleUidTimeReader;
 import com.android.internal.os.PowerProfile;
 import com.android.internal.power.EnergyConsumerStats;
+import com.android.server.power.optimization.Flags;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -54,6 +58,8 @@
 @RunWith(AndroidJUnit4.class)
 @SuppressWarnings("GuardedBy")
 public class SystemServicePowerCalculatorTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final double PRECISION = 0.000001;
     private static final int APP_UID1 = 100;
@@ -108,6 +114,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
     public void testPowerProfileBasedModel() {
         prepareBatteryStats(null);
 
@@ -135,6 +142,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
     public void testMeasuredEnergyBasedModel() {
         final boolean[] supportedPowerBuckets =
                 new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index e22d99d..5611415 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -84,6 +84,7 @@
         "flag-junit",
         "ravenwood-junit",
         "net_flags_lib",
+        "CtsVirtualDeviceCommonLib",
     ],
 
     libs: [
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
index a2aaccc..b487dc6 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.appop;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
@@ -31,17 +33,23 @@
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Tests app ops version upgrades
@@ -50,6 +58,8 @@
 @RunWith(AndroidJUnit4.class)
 public class AppOpsActiveWatcherTest {
 
+    @Rule
+    public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
     private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
 
     @Test
@@ -69,7 +79,7 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                isNull(), eq(true), anyInt(), anyInt());
+                isNull(), eq(Context.DEVICE_ID_DEFAULT), eq(true), anyInt(), anyInt());
 
         // This should be the only callback we got
         verifyNoMoreInteractions(listener);
@@ -88,7 +98,7 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()), isNull(),
-                eq(false), anyInt(), anyInt());
+                eq(Context.DEVICE_ID_DEFAULT), eq(false), anyInt(), anyInt());
 
         // Verify that the op is not active
         assertThat(appOpsManager.isOperationActive(AppOpsManager.OP_CAMERA,
@@ -126,7 +136,7 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()), isNull(),
-                eq(true), anyInt(), anyInt());
+                eq(Context.DEVICE_ID_DEFAULT), eq(true), anyInt(), anyInt());
 
         // Finish up
         appOpsManager.finishOp(AppOpsManager.OP_CAMERA);
@@ -134,6 +144,64 @@
     }
 
     @Test
+    public void testWatchActiveOpsForExternalDevice() {
+        final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+                VirtualDeviceManager.class);
+        AtomicInteger virtualDeviceId = new AtomicInteger();
+        runWithShellPermissionIdentity(() -> {
+            final VirtualDeviceManager.VirtualDevice virtualDevice =
+                    virtualDeviceManager.createVirtualDevice(
+                            mFakeAssociationRule.getAssociationInfo().getId(),
+                            new VirtualDeviceParams.Builder().setName("virtual_device").build());
+            virtualDeviceId.set(virtualDevice.getDeviceId());
+        });
+        final OnOpActiveChangedListener listener = mock(OnOpActiveChangedListener.class);
+        AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+                getContext().getOpPackageName(), getContext().getAttributionTag(),
+                virtualDeviceId.get());
+
+        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        appOpsManager.startWatchingActive(new String[]{AppOpsManager.OPSTR_CAMERA,
+                AppOpsManager.OPSTR_RECORD_AUDIO}, getContext().getMainExecutor(), listener);
+
+        appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_CAMERA, attributionSource, false, "",
+                AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+        verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
+                eq(Process.myUid()), eq(getContext().getOpPackageName()),
+                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(true),
+                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+        verifyNoMoreInteractions(listener);
+
+        appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_CAMERA, attributionSource);
+
+        verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
+                eq(Process.myUid()), eq(getContext().getOpPackageName()),
+                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(false),
+                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+        verifyNoMoreInteractions(listener);
+
+        appOpsManager.stopWatchingActive(listener);
+
+        appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_CAMERA, attributionSource, false, "",
+                AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+        verifyNoMoreInteractions(listener);
+
+        appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_CAMERA, attributionSource);
+
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
     public void testIsRunning() throws Exception {
         final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
         // Start the op
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
index b5229d8..1abd4eb 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
@@ -22,27 +22,36 @@
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpNotedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * Tests watching noted ops.
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AppOpsNotedWatcherTest {
-
+    @Rule
+    public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
     private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
 
     @Test
@@ -66,12 +75,14 @@
         inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF),
                 eq(AppOpsManager.MODE_ALLOWED));
         inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF),
                 eq(AppOpsManager.MODE_ALLOWED));
 
         // Stop watching
@@ -96,13 +107,54 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(2)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF),
                 eq(AppOpsManager.MODE_ALLOWED));
 
         // Finish up
         appOpsManager.stopWatchingNoted(listener);
     }
 
+    @Test
+    public void testWatchNotedOpsForExternalDevice() {
+        final AppOpsManager.OnOpNotedListener listener = mock(
+                AppOpsManager.OnOpNotedListener.class);
+        final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+                VirtualDeviceManager.class);
+        AtomicInteger virtualDeviceId = new AtomicInteger();
+        runWithShellPermissionIdentity(() -> {
+            final VirtualDeviceManager.VirtualDevice virtualDevice =
+                    virtualDeviceManager.createVirtualDevice(
+                            mFakeAssociationRule.getAssociationInfo().getId(),
+                            new VirtualDeviceParams.Builder().setName("virtual_device").build());
+            virtualDeviceId.set(virtualDevice.getDeviceId());
+        });
+        AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+                getContext().getOpPackageName(), getContext().getAttributionTag(),
+                virtualDeviceId.get());
+
+        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        appOpsManager.startWatchingNoted(new int[]{AppOpsManager.OP_FINE_LOCATION,
+                AppOpsManager.OP_CAMERA}, listener);
+
+        appOpsManager.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, attributionSource, "message");
+
+        verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
+                eq(Process.myUid()), eq(getContext().getOpPackageName()),
+                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+                eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED));
+
+        appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_FINE_LOCATION, attributionSource);
+
+        verifyNoMoreInteractions(listener);
+
+        appOpsManager.stopWatchingNoted(listener);
+
+        verifyNoMoreInteractions(listener);
+    }
+
     private static Context getContext() {
         return InstrumentationRegistry.getContext();
     }
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
index e98a4dd..2890078 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.appop;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -25,22 +27,31 @@
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpStartedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 /** Tests watching started ops. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AppOpsStartedWatcherTest {
 
+    @Rule
+    public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
     private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
 
     @Test
@@ -63,15 +74,17 @@
         inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
-                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+                eq(OnOpStartedListener.START_TYPE_STARTED),
                 eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                 eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
         inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(1)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
-                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+                eq(OnOpStartedListener.START_TYPE_STARTED),
                 eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                 eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
 
@@ -97,8 +110,9 @@
         verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                 .times(2)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                 eq(Process.myUid()), eq(getContext().getPackageName()),
-                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
-                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+                eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+                eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+                eq(OnOpStartedListener.START_TYPE_STARTED),
                 eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                 eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
         verifyNoMoreInteractions(listener);
@@ -109,6 +123,50 @@
         appOpsManager.stopWatchingStarted(listener);
     }
 
+    @Test
+    public void testWatchStartedOpsForExternalDevice() {
+        final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+                VirtualDeviceManager.class);
+        AtomicInteger virtualDeviceId = new AtomicInteger();
+        runWithShellPermissionIdentity(() -> {
+            final VirtualDeviceManager.VirtualDevice virtualDevice =
+                    virtualDeviceManager.createVirtualDevice(
+                            mFakeAssociationRule.getAssociationInfo().getId(),
+                            new VirtualDeviceParams.Builder().setName("virtual_device").build());
+            virtualDeviceId.set(virtualDevice.getDeviceId());
+        });
+        final OnOpStartedListener listener = mock(OnOpStartedListener.class);
+        AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+                getContext().getOpPackageName(), getContext().getAttributionTag(),
+                virtualDeviceId.get());
+
+        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_FINE_LOCATION,
+                AppOpsManager.OP_CAMERA}, listener);
+
+        appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_FINE_LOCATION, attributionSource, false,
+                "message", 0, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+        verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
+                eq(Process.myUid()), eq(getContext().getOpPackageName()),
+                eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+                eq(AppOpsManager.OP_FLAG_SELF),
+                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+
+        appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+                AppOpsManager.OP_FINE_LOCATION, attributionSource);
+
+        verifyNoMoreInteractions(listener);
+
+        appOpsManager.stopWatchingStarted(listener);
+
+        verifyNoMoreInteractions(listener);
+    }
+
     private static Context getContext() {
         return InstrumentationRegistry.getContext();
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
index 3a9c0f0..a1f0dbd 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
@@ -70,7 +70,7 @@
         mClientMonitor.binderDied();
 
         assertThat(mClientMonitor.mCanceled).isTrue();
-        assertThat(mClientMonitor.getListener()).isNull();
+        assertThat(mClientMonitor.getListener()).isNotNull();
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
index c8bfaa9..5b81277 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
@@ -87,14 +87,6 @@
         verify(mCallback).onClientFinished(mClient, true);
     }
 
-    @Test
-    public void generateChallenge_nullListener() {
-        createClient(null);
-        mClient.start(mCallback);
-
-        verify(mCallback).onClientFinished(mClient, false);
-    }
-
     private void createClient(ClientMonitorCallbackConverter listener) {
         mClient = new FaceGenerateChallengeClient(mContext, () -> mAidlSession, mToken, listener,
                 USER_ID, TAG, SENSOR_ID, mBiometricLogger, mBiometricContext);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index 07dd59d2..a4628ee 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -18,6 +18,8 @@
 
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.content.Context.DEVICE_ID_INVALID;
@@ -135,4 +137,34 @@
         when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS)).thenReturn(DEVICE_POLICY_CUSTOM);
         assertThat(virtualDevice.hasCustomSensorSupport()).isTrue();
     }
+
+    @Test
+    public void virtualDevice_hasCustomAudioInputSupport() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS);
+
+        VirtualDevice virtualDevice =
+                new VirtualDevice(
+                        mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_DEFAULT);
+        assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse();
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM);
+        assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue();
+    }
+
+    @Test
+    public void virtualDevice_hasCustomCameraSupport() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS);
+
+        VirtualDevice virtualDevice =
+                new VirtualDevice(
+                        mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA)).thenReturn(DEVICE_POLICY_DEFAULT);
+        assertThat(virtualDevice.hasCustomCameraSupport()).isFalse();
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA)).thenReturn(DEVICE_POLICY_CUSTOM);
+        assertThat(virtualDevice.hasCustomCameraSupport()).isTrue();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
index d850c73..57f3cc0 100644
--- a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
@@ -22,6 +22,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 
+import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -40,10 +41,12 @@
 public final class CredentialManagerServiceTest {
 
     Context mContext = null;
+    MockSettingsWrapper mSettingsWrapper = null;
 
     @Before
     public void setUp() throws CertificateException {
         mContext = ApplicationProvider.getApplicationContext();
+        mSettingsWrapper = new MockSettingsWrapper(mContext);
     }
 
     @Test
@@ -81,7 +84,8 @@
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
                 "com.example.test/com.example.test.TestActivity");
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test");
 
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
         assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -101,7 +105,8 @@
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test3");
 
         // Since the provider removed was not a primary provider then we should do nothing.
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE))
@@ -125,7 +130,8 @@
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
                 "com.example.test/com.example.test.TestActivity");
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test");
 
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
         assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -144,7 +150,8 @@
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test3");
 
         // Since the provider removed was not a primary provider then we should do nothing.
         assertCredentialPropertyEquals(
@@ -176,12 +183,36 @@
         assertThat(actualValueSet).isEqualTo(newValueSet);
     }
 
-    private void setSettingsKey(String key, String value) {
-        assertThat(Settings.Secure.putString(mContext.getContentResolver(), key, value)).isTrue();
+    private void setSettingsKey(String name, String value) {
+        assertThat(
+                        mSettingsWrapper.putStringForUser(
+                                name, value, UserHandle.myUserId(), true))
+                .isTrue();
     }
 
-    private String getSettingsKey(String key) {
-        return Settings.Secure.getStringForUser(
-                mContext.getContentResolver(), key, UserHandle.myUserId());
+    private String getSettingsKey(String name) {
+        return mSettingsWrapper.getStringForUser(name, UserHandle.myUserId());
+    }
+
+    private static final class MockSettingsWrapper
+            extends CredentialManagerService.SettingsWrapper {
+
+        MockSettingsWrapper(@NonNull Context context) {
+            super(context);
+        }
+
+        /** Updates the string value of a system setting */
+        @Override
+        public boolean putStringForUser(
+                String name,
+                String value,
+                int userHandle,
+                boolean overrideableByRestore) {
+            // This will ensure that when the settings putStringForUser method is called by
+            // CredentialManagerService that the overrideableByRestore bit is true.
+            assertThat(overrideableByRestore).isTrue();
+
+            return Settings.Secure.putStringForUser(getContentResolver(), name, value, userHandle);
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index 3de167e..dacff4c 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -23,7 +23,6 @@
 
 import android.content.Context;
 import android.graphics.FontListParser;
-import android.graphics.fonts.FontFamily;
 import android.graphics.fonts.FontManager;
 import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontUpdateRequest;
@@ -324,15 +323,16 @@
         Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
             FontConfig.Font fooFont = new FontConfig.Font(
                     new File(mPreinstalledFontDirs.get(0), "foo.ttf"), null, "foo",
-                    new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
+                    new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null,
+                    FontConfig.Font.VAR_TYPE_AXES_NONE);
             FontConfig.Font barFont = new FontConfig.Font(
                     new File(mPreinstalledFontDirs.get(1), "bar.ttf"), null, "bar",
-                    new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
+                    new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null,
+                    FontConfig.Font.VAR_TYPE_AXES_NONE);
 
             FontConfig.FontFamily family = new FontConfig.FontFamily(
                     Arrays.asList(fooFont, barFont), null,
-                    FontConfig.FontFamily.VARIANT_DEFAULT,
-                    FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                    FontConfig.FontFamily.VARIANT_DEFAULT);
             return new FontConfig(Collections.emptyList(),
                     Collections.emptyList(),
                     Collections.singletonList(new FontConfig.NamedFamilyList(
@@ -492,10 +492,9 @@
                 mConfigFile, mCurrentTimeSupplier, (map) -> {
             FontConfig.Font font = new FontConfig.Font(
                     file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT),
-                    0, null, null);
+                    0, null, null, FontConfig.Font.VAR_TYPE_AXES_NONE);
             FontConfig.FontFamily family = new FontConfig.FontFamily(
-                    Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT,
-                    FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                    Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
             return new FontConfig(
                     Collections.emptyList(),
                     Collections.emptyList(),
@@ -647,10 +646,9 @@
                 mConfigFile, mCurrentTimeSupplier, (map) -> {
             FontConfig.Font font = new FontConfig.Font(
                     file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null,
-                    null);
+                    null, FontConfig.Font.VAR_TYPE_AXES_NONE);
             FontConfig.FontFamily family = new FontConfig.FontFamily(
-                    Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT,
-                    FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+                    Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
                     Collections.singletonList(new FontConfig.NamedFamilyList(
                             Collections.singletonList(family), "sans-serif")),
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 1172a87..4641802 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
 import static com.android.server.hdmi.OneTouchPlayAction.STATE_WAITING_FOR_REPORT_POWER_STATUS;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -46,6 +47,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link OneTouchPlayAction} */
 @SmallTest
@@ -70,6 +72,7 @@
 
     private Context mContextSpy;
     private HdmiControlService mHdmiControlService;
+    private HdmiCecController mHdmiCecController;
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
     private FakeHdmiCecConfig mHdmiCecConfig;
@@ -108,10 +111,10 @@
         mHdmiControlService.setHdmiCecConfig(mHdmiCecConfig);
         setHdmiControlEnabled(hdmiControlEnabled);
         mNativeWrapper = new FakeNativeWrapper();
-        HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
+        mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
-        mHdmiControlService.setCecController(hdmiCecController);
+        mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
@@ -618,6 +621,53 @@
         assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
     }
 
+    @Test
+    public void onWakeUp_notInteractive_startOneTouchPlay() throws Exception {
+        setUp(true);
+
+        mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+        mPowerManager.setInteractive(false);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPowerManager.isInteractive()).isFalse();
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage textViewOn =
+                HdmiCecMessageBuilder.buildTextViewOn(
+                        mHdmiControlService.playback().getDeviceInfo().getLogicalAddress(),
+                        ADDR_TV);
+        assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+    }
+
+
+    @Test
+    public void onWakeUp_interruptedByOnStandby_notInteractive_OneTouchPlayNotStarted()
+            throws Exception {
+        setUp(true);
+        long allocationDelay = TimeUnit.SECONDS.toMillis(1);
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mTestLooper.dispatchAll();
+
+        mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        mPowerManager.setInteractive(false);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPowerManager.isInteractive()).isFalse();
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage textViewOn =
+                HdmiCecMessageBuilder.buildTextViewOn(
+                        mHdmiControlService.playback().getDeviceInfo().getLogicalAddress(),
+                        ADDR_TV);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(textViewOn);
+
+    }
+
     private static class TestActionTimer implements ActionTimer {
         private int mState;
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index eca19c8..2da2f50 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -506,6 +506,14 @@
     }
 
     @Test
+    public void testUnlockUserWithTokenWithBadHandleReturnsFalse() {
+        final long badTokenHandle = 123456789;
+        final byte[] token = "some-high-entropy-secure-token".getBytes();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        assertFalse(mLocalService.unlockUserWithToken(badTokenHandle, token, PRIMARY_USER_ID));
+    }
+
+    @Test
     public void testGetHashFactorPrimaryUser() throws RemoteException {
         LockscreenCredential password = newPassword("password");
         initSpAndSetCredential(PRIMARY_USER_ID, password);
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 0805485..81df597 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -157,6 +157,7 @@
                     return mMockDevicePolicyManager;
                 case Context.APP_SEARCH_SERVICE:
                 case Context.ROLE_SERVICE:
+                case Context.APP_OPS_SERVICE:
                     // RoleManager is final and cannot be mocked, so we only override the inject
                     // accessor methods in ShortcutService.
                     return getTestContext().getSystemService(name);
diff --git a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
index 398148f..2e5feff 100644
--- a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
@@ -273,7 +273,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final IAppOpsCallback watcher = new IAppOpsCallback.Stub() {
             @Override
-            public void opChanged(int op, int uid, String packageName) {
+            public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
                 if (op == code && packageName.equals(TEST_APP_PACKAGE_NAME)) {
                     latch.countDown();
                 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 9c2cba8..ef0ac33 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -227,8 +227,6 @@
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.platform.test.rule.DeniedDevices;
-import android.platform.test.rule.DeviceProduct;
 import android.platform.test.rule.LimitDevicesRule;
 import android.provider.DeviceConfig;
 import android.provider.MediaStore;
@@ -336,7 +334,6 @@
 @RunWith(AndroidTestingRunner.class)
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
 @RunWithLooper
-@DeniedDevices(denied = {DeviceProduct.CF_AUTO})
 public class NotificationManagerServiceTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
     private static final String TEST_PACKAGE = "The.name.is.Package.Test.Package";
@@ -593,7 +590,7 @@
         when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt()))
                 .thenReturn(INVALID_TASK_ID);
         mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
-        when(mUm.getProfileIds(0, false)).thenReturn(new int[]{0});
+        when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId });
 
         when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true);
 
@@ -881,9 +878,7 @@
     private void simulatePackageRemovedBroadcast(String pkg, int uid) {
         // mimics receive broadcast that package is removed, but doesn't remove the package.
         final Bundle extras = new Bundle();
-        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST,
-                new String[]{pkg});
-        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, new int[]{uid});
+        extras.putInt(Intent.EXTRA_UID, uid);
 
         final Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
         intent.setData(Uri.parse("package:" + pkg));
@@ -1031,7 +1026,7 @@
 
     private NotificationRecord generateNotificationRecord(NotificationChannel channel,
             long postTime) {
-        final StatusBarNotification sbn = generateSbn(PKG, mUid, postTime, 0);
+        final StatusBarNotification sbn = generateSbn(PKG, mUid, postTime, mUserId);
         return new NotificationRecord(mContext, sbn, channel);
     }
 
@@ -1766,7 +1761,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testEnqueueNotification_appBlocked", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
         verify(mWorkerHandler, never()).post(
                 any(NotificationManagerService.EnqueueNotificationRunnable.class));
@@ -1776,7 +1771,7 @@
     public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
         StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG);
         assertEquals(1, notifs.length);
@@ -1787,7 +1782,7 @@
     public void testEnqueueNotificationWithTag_WritesExpectedLogs() throws Exception {
         final String tag = "testEnqueueNotificationWithTag_WritesExpectedLog";
         mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
         assertEquals(1, mNotificationRecordLogger.numCalls());
 
@@ -1828,12 +1823,12 @@
         Notification original = new Notification.Builder(mContext,
                 mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, original, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, original, mUserId);
         Notification update = new Notification.Builder(mContext,
                 mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .setCategory(Notification.CATEGORY_ALARM).build();
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, update, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, update, mUserId);
         waitForIdle();
         assertEquals(2, mNotificationRecordLogger.numCalls());
 
@@ -1853,9 +1848,9 @@
     public void testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate() throws Exception {
         final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate";
         mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
         assertEquals(2, mNotificationRecordLogger.numCalls());
         assertTrue(mNotificationRecordLogger.get(0).wasLogged);
@@ -1869,10 +1864,10 @@
         final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnTitleUpdate";
         mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
                 generateNotificationRecord(null).getNotification(),
-                0);
+                mUserId);
         final Notification notif = generateNotificationRecord(null).getNotification();
         notif.extras.putString(Notification.EXTRA_TITLE, "Changed title");
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notif, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notif, mUserId);
         waitForIdle();
         assertEquals(2, mNotificationRecordLogger.numCalls());
         assertEquals(NOTIFICATION_POSTED, mNotificationRecordLogger.event(0));
@@ -1885,11 +1880,11 @@
         Notification notification = new Notification.Builder(mContext,
                 mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, mUserId);
         waitForIdle();
-        mBinderService.cancelNotificationWithTag(PKG, PKG, tag, 0, 0);
+        mBinderService.cancelNotificationWithTag(PKG, PKG, tag, 0, mUserId);
         waitForIdle();
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, mUserId);
         waitForIdle();
         assertEquals(3, mNotificationRecordLogger.numCalls());
 
@@ -1949,7 +1944,7 @@
                 .build();
         n.actions[1] = null;
 
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId);
         waitForIdle();
 
         StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
@@ -1970,7 +1965,7 @@
         n.actions[0] = null;
         n.actions[1] = null;
 
-        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId);
         waitForIdle();
 
         StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
@@ -1982,7 +1977,7 @@
     public void enqueueNotificationWithTag_usesAndFinishesTracker() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
 
         assertThat(mPostNotificationTrackerFactory.mCreatedTrackers).hasSize(1);
         assertThat(mPostNotificationTrackerFactory.mCreatedTrackers.get(0).isOngoing()).isTrue();
@@ -2000,7 +1995,7 @@
         assertThrows(Exception.class,
                 () -> mBinderService.enqueueNotificationWithTag(PKG, PKG,
                         "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0,
-                        /* notification= */ null, 0));
+                        /* notification= */ null, mUserId));
 
         waitForIdle();
 
@@ -2017,7 +2012,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
 
         assertThat(mBinderService.getActiveNotifications(PKG)).hasLength(0);
@@ -2032,7 +2027,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
 
         assertThat(mBinderService.getActiveNotifications(PKG)).hasLength(0);
@@ -2044,7 +2039,7 @@
     public void enqueueNotification_acquiresAndReleasesWakeLock() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "enqueueNotification_acquiresAndReleasesWakeLock", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
 
         verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString());
         assertThat(mAcquiredWakeLocks).hasSize(1);
@@ -2062,7 +2057,7 @@
         assertThrows(Exception.class,
                 () -> mBinderService.enqueueNotificationWithTag(PKG, PKG,
                         "enqueueNotification_throws_acquiresAndReleasesWakeLock", 0,
-                        /* notification= */ null, 0));
+                        /* notification= */ null, mUserId));
 
         verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString());
         assertThat(mAcquiredWakeLocks).hasSize(1);
@@ -2077,7 +2072,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "enqueueNotification_notEnqueued_acquiresAndReleasesWakeLock", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
 
         verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString());
         assertThat(mAcquiredWakeLocks).hasSize(1);
@@ -2098,7 +2093,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "enqueueNotification_notPosted_acquiresAndReleasesWakeLock", 0,
-                notif, 0);
+                notif, mUserId);
 
         verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString());
         assertThat(mAcquiredWakeLocks).hasSize(1);
@@ -2123,7 +2118,7 @@
 
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "enqueueNotification_setsWakeLockWorkSource", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
 
         InOrder inOrder = inOrder(mPowerManager, wakeLock);
@@ -2137,7 +2132,7 @@
     @Test
     public void testCancelNonexistentNotification() throws Exception {
         mBinderService.cancelNotificationWithTag(PKG, PKG,
-                "testCancelNonexistentNotification", 0, 0);
+                "testCancelNonexistentNotification", 0, mUserId);
         waitForIdle();
         // The notification record logger doesn't even get called when a nonexistent notification
         // is cancelled, because that happens very frequently and is not interesting.
@@ -2148,9 +2143,9 @@
     public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testCancelNotificationImmediatelyAfterEnqueue", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         mBinderService.cancelNotificationWithTag(PKG, PKG,
-                "testCancelNotificationImmediatelyAfterEnqueue", 0, 0);
+                "testCancelNotificationImmediatelyAfterEnqueue", 0, mUserId);
         waitForIdle();
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(PKG);
@@ -2185,13 +2180,13 @@
     public void testCancelNotificationWhilePostedAndEnqueued() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testCancelNotificationWhilePostedAndEnqueued", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         waitForIdle();
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testCancelNotificationWhilePostedAndEnqueued", 0,
-                generateNotificationRecord(null).getNotification(), 0);
+                generateNotificationRecord(null).getNotification(), mUserId);
         mBinderService.cancelNotificationWithTag(PKG, PKG,
-                "testCancelNotificationWhilePostedAndEnqueued", 0, 0);
+                "testCancelNotificationWhilePostedAndEnqueued", 0, mUserId);
         waitForIdle();
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(PKG);
@@ -3406,12 +3401,12 @@
     @Test
     public void testPostNotification_appPermissionFixed() throws Exception {
         when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
-        when(mPermissionHelper.isPermissionFixed(PKG, 0)).thenReturn(true);
+        when(mPermissionHelper.isPermissionFixed(PKG, mUserId)).thenReturn(true);
 
         NotificationRecord temp = generateNotificationRecord(mTestNotificationChannel);
         mBinderService.enqueueNotificationWithTag(PKG, PKG,
                 "testPostNotification_appPermissionFixed", 0,
-                temp.getNotification(), 0);
+                temp.getNotification(), mUserId);
         waitForIdle();
         assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
         StatusBarNotification[] notifs =
@@ -3443,7 +3438,7 @@
 
         Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_onTv", 0,
-                generateNotificationRecord(null, tv).getNotification(), 0);
+                generateNotificationRecord(null, tv).getNotification(), mUserId);
         verify(mPreferencesHelper, times(1)).getConversationNotificationChannel(
                 anyString(), anyInt(), eq("foo"), eq(null), anyBoolean(), anyBoolean());
     }
@@ -3458,7 +3453,7 @@
 
         Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_notOnTv",
-                0, generateNotificationRecord(null, tv).getNotification(), 0);
+                0, generateNotificationRecord(null, tv).getNotification(), mUserId);
         verify(mPreferencesHelper, times(1)).getConversationNotificationChannel(
                 anyString(), anyInt(), eq(mTestNotificationChannel.getId()), eq(null),
                 anyBoolean(), anyBoolean());
@@ -11859,10 +11854,10 @@
 
     @Test
     public void testGetActiveNotification_filtersUsers() throws Exception {
-        when(mUm.getProfileIds(0, false)).thenReturn(new int[]{0, 10});
+        when(mUm.getProfileIds(mUserId, false)).thenReturn(new int[]{mUserId, 10});
 
         NotificationRecord nr0 =
-                generateNotificationRecord(mTestNotificationChannel, 0);
+                generateNotificationRecord(mTestNotificationChannel, mUserId);
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0",
                 nr0.getSbn().getId(), nr0.getSbn().getNotification(), nr0.getSbn().getUserId());
 
@@ -12316,7 +12311,7 @@
                 .setFullScreenIntent(mock(PendingIntent.class), true)
                 .build();
 
-        mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+        mService.fixNotification(n, PKG, "tag", 9, mUserId, mUid, NOT_FOREGROUND_SERVICE, true);
 
         final int stickyFlag = n.flags & Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 87e822c..5d114f4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -2123,6 +2123,25 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testDefaultRulesFromConfig_modesApi_getPolicies() {
+        // After mZenModeHelper was created, set some things in the policy so it's changed from
+        // default.
+        setupZenConfig();
+
+        // Find default rules; check they have non-null policies; check that they match the default
+        // and not whatever has been set up in setupZenConfig.
+        ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+        for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) {
+            assertThat(rules).containsKey(defaultId);
+            ZenRule rule = rules.get(defaultId);
+            assertThat(rule.zenPolicy).isNotNull();
+
+            assertThat(rule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+        }
+    }
+
+    @Test
     public void testAddAutomaticZenRule_beyondSystemLimit() {
         for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) {
             ScheduleInfo si = new ScheduleInfo();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index f9fe6a9..b431888 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -53,7 +53,6 @@
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
-import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -270,10 +269,8 @@
         setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
         setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
 
-        SparseArray<Float> adaptiveHapticsScales = new SparseArray<>();
-        adaptiveHapticsScales.put(USAGE_RINGTONE, 0.5f);
-        adaptiveHapticsScales.put(USAGE_NOTIFICATION, 0.5f);
-        mVibrationScaler.updateAdaptiveHapticsScales(adaptiveHapticsScales);
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
 
         StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
                 VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
@@ -287,6 +284,50 @@
         assertTrue(scaled.getAmplitude() < 0.5);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    public void scale_clearAdaptiveHapticsScales_clearsAllCachedScales() {
+        setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
+        setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
+
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
+        mVibrationScaler.clearAdaptiveHapticsScales();
+
+        StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
+                VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
+        // Ringtone scales up.
+        assertTrue(scaled.getAmplitude() > 0.5);
+
+        scaled = getFirstSegment(mVibrationScaler.scale(
+                VibrationEffect.createWaveform(new long[]{128}, new int[]{128}, -1),
+                USAGE_NOTIFICATION));
+        // Notification scales up.
+        assertTrue(scaled.getAmplitude() > 0.5);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    public void scale_removeAdaptiveHapticsScale_removesCachedScale() {
+        setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
+        setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
+
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+        mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
+        mVibrationScaler.removeAdaptiveHapticsScale(USAGE_NOTIFICATION);
+
+        StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
+                VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
+        // Ringtone scales down.
+        assertTrue(scaled.getAmplitude() < 0.5);
+
+        scaled = getFirstSegment(mVibrationScaler.scale(
+                VibrationEffect.createWaveform(new long[]{128}, new int[]{128}, -1),
+                USAGE_NOTIFICATION));
+        // Notification scales up.
+        assertTrue(scaled.getAmplitude() > 0.5);
+    }
+
     private void setDefaultIntensity(@VibrationAttributes.Usage int usage,
             @Vibrator.VibrationIntensity int intensity) {
         when(mVibrationConfigMock.getDefaultVibrationIntensity(eq(usage))).thenReturn(intensity);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index b0aef47..6e478d8 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import static android.os.VibrationAttributes.USAGE_RINGTONE;
 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
 
@@ -54,12 +55,16 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.test.TestLooper;
+import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
@@ -81,6 +86,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
@@ -95,6 +101,9 @@
     private static final int TEST_RAMP_STEP_DURATION = 5;
 
     @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Mock private PackageManagerInternal mPackageManagerInternalMock;
     @Mock private VibrationThread.VibratorManagerHooks mManagerHooks;
@@ -104,6 +113,7 @@
 
     private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
     private VibrationSettings mVibrationSettings;
+    private VibrationScaler mVibrationScaler;
     private TestLooper mTestLooper;
     private TestLooperAutoDispatcher mCustomTestLooperDispatcher;
     private VibrationThread mThread;
@@ -132,6 +142,7 @@
         Context context = InstrumentationRegistry.getContext();
         mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
                 mVibrationConfigMock);
+        mVibrationScaler = new VibrationScaler(context, mVibrationSettings);
 
         mockVibrators(VIBRATOR_ID);
 
@@ -231,6 +242,45 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
+        CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.runAsync(() -> {
+            mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+        });
+        long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
+                USAGE_RINGTONE);
+        waitForCompletion();
+
+        assertEquals(Arrays.asList(expectedOneShot(15)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+        List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
+        for (int i = 0; i < amplitudes.size(); i++) {
+            assertTrue(amplitudes.get(i) < 1 / 255f);
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+    public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
+
+        CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>();
+        long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE);
+        waitForCompletion();
+
+        assertEquals(Arrays.asList(expectedOneShot(15)),
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+        assertEquals(expectedAmplitudes(1, 1, 1),
+                mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+    }
+
+    @Test
     public void vibrate_singleVibratorRepeatingWaveform_runsVibrationUntilThreadCancelled()
             throws Exception {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -1610,10 +1660,26 @@
     }
 
     private long startThreadAndDispatcher(HalVibration vib) {
+        return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null);
+    }
+
+    private long startThreadAndDispatcher(VibrationEffect effect,
+            CompletableFuture<Void> requestVibrationParamsFuture, int usage) {
+        VibrationAttributes attrs = new VibrationAttributes.Builder()
+                .setUsage(usage)
+                .build();
+        HalVibration vib = new HalVibration(mVibrationToken,
+                CombinedVibration.createParallel(effect),
+                new Vibration.CallerInfo(attrs, UID, DEVICE_ID, PACKAGE_NAME, "reason"));
+        return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
+    }
+
+    private long startThreadAndDispatcher(HalVibration vib,
+            CompletableFuture<Void> requestVibrationParamsFuture) {
         mControllers = createVibratorControllers();
         DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers);
-        mVibrationConductor =
-                new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter, mManagerHooks);
+        mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter,
+                mVibrationScaler, requestVibrationParamsFuture, mManagerHooks);
         assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor));
         return mVibrationConductor.getVibration().id;
     }
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index 1e0b1df..2823223 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -18,29 +18,47 @@
 
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
+import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
+import static android.os.VibrationAttributes.USAGE_RINGTONE;
+import static android.os.VibrationAttributes.USAGE_TOUCH;
+import static android.os.VibrationAttributes.USAGE_UNKNOWN;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
 import android.frameworks.vibrator.ScaleParam;
 import android.frameworks.vibrator.VibrationParam;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.test.TestLooper;
 import android.util.SparseArray;
 
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 public class VibratorControlServiceTest {
 
@@ -49,35 +67,45 @@
 
     @Mock
     private VibrationScaler mMockVibrationScaler;
-    @Captor
-    private ArgumentCaptor<SparseArray<Float>> mVibrationScalesCaptor;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternalMock;
 
+    private FakeVibratorController mFakeVibratorController;
     private VibratorControlService mVibratorControlService;
+    private VibrationSettings mVibrationSettings;
     private final Object mLock = new Object();
 
     @Before
     public void setUp() throws Exception {
+        when(mPackageManagerInternalMock.getSystemUiServiceComponent())
+                .thenReturn(new ComponentName("", ""));
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
+
+        TestLooper testLooper = new TestLooper();
+        mVibrationSettings = new VibrationSettings(
+                ApplicationProvider.getApplicationContext(), new Handler(testLooper.getLooper()));
+
+        mFakeVibratorController = new FakeVibratorController();
         mVibratorControlService = new VibratorControlService(new VibratorControllerHolder(),
-                mMockVibrationScaler, mLock);
+                mMockVibrationScaler, mVibrationSettings, mLock);
     }
 
     @Test
     public void testRegisterVibratorController() throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-        mVibratorControlService.registerVibratorController(fakeController);
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
 
-        assertThat(fakeController.isLinkedToDeath).isTrue();
+        assertThat(mFakeVibratorController.isLinkedToDeath).isTrue();
     }
 
     @Test
     public void testUnregisterVibratorController_providingTheRegisteredController_performsRequest()
             throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-        mVibratorControlService.registerVibratorController(fakeController);
-        mVibratorControlService.unregisterVibratorController(fakeController);
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+        mVibratorControlService.unregisterVibratorController(mFakeVibratorController);
 
-        verify(mMockVibrationScaler).updateAdaptiveHapticsScales(null);
-        assertThat(fakeController.isLinkedToDeath).isFalse();
+        verify(mMockVibrationScaler).clearAdaptiveHapticsScales();
+        assertThat(mFakeVibratorController.isLinkedToDeath).isFalse();
     }
 
     @Test
@@ -93,41 +121,80 @@
     }
 
     @Test
+    public void testOnRequestVibrationParamsComplete_cachesAdaptiveHapticsScalesCorrectly()
+            throws RemoteException {
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+        int timeoutInMillis = 10;
+        CompletableFuture<Void> future =
+                mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+                        timeoutInMillis);
+        IBinder token = mVibratorControlService.getRequestVibrationParamsToken();
+
+        SparseArray<Float> vibrationScales = new SparseArray<>();
+        vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+        vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+        mVibratorControlService.onRequestVibrationParamsComplete(token,
+                generateVibrationParams(vibrationScales));
+
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f);
+        // Setting ScaleParam.TYPE_NOTIFICATION will update vibration scaling for both
+        // notification and communication request usages.
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, 0.4f);
+        verifyNoMoreInteractions(mMockVibrationScaler);
+
+        assertThat(future.isDone()).isTrue();
+        assertThat(future.isCompletedExceptionally()).isFalse();
+    }
+
+    @Test
+    public void testOnRequestVibrationParamsComplete_withIncorrectToken_ignoresRequest()
+            throws RemoteException, InterruptedException {
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+        int timeoutInMillis = 10;
+        CompletableFuture<Void> unusedFuture =
+                mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+                        timeoutInMillis);
+
+        SparseArray<Float> vibrationScales = new SparseArray<>();
+        vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+        vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+        mVibratorControlService.onRequestVibrationParamsComplete(new Binder(),
+                generateVibrationParams(vibrationScales));
+
+        verifyZeroInteractions(mMockVibrationScaler);
+    }
+
+    @Test
     public void testSetVibrationParams_cachesAdaptiveHapticsScalesCorrectly()
             throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-        mVibratorControlService.registerVibratorController(fakeController);
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
         SparseArray<Float> vibrationScales = new SparseArray<>();
         vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
         vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
 
         mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
-                fakeController);
+                mFakeVibratorController);
 
-        verify(mMockVibrationScaler).updateAdaptiveHapticsScales(mVibrationScalesCaptor.capture());
-        SparseArray<Float> cachedVibrationScales = mVibrationScalesCaptor.getValue();
-        assertThat(cachedVibrationScales.size()).isEqualTo(3);
-        assertThat(cachedVibrationScales.keyAt(0)).isEqualTo(USAGE_ALARM);
-        assertThat(cachedVibrationScales.valueAt(0)).isEqualTo(0.7f);
-        assertThat(cachedVibrationScales.keyAt(1)).isEqualTo(USAGE_NOTIFICATION);
-        assertThat(cachedVibrationScales.valueAt(1)).isEqualTo(0.4f);
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f);
         // Setting ScaleParam.TYPE_NOTIFICATION will update vibration scaling for both
         // notification and communication request usages.
-        assertThat(cachedVibrationScales.keyAt(2)).isEqualTo(USAGE_COMMUNICATION_REQUEST);
-        assertThat(cachedVibrationScales.valueAt(2)).isEqualTo(0.4f);
+        verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, 0.4f);
+        verifyNoMoreInteractions(mMockVibrationScaler);
     }
 
     @Test
     public void testSetVibrationParams_withUnregisteredController_ignoresRequest()
             throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-
         SparseArray<Float> vibrationScales = new SparseArray<>();
         vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
         vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
 
         mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
-                fakeController);
+                mFakeVibratorController);
 
         verifyZeroInteractions(mMockVibrationScaler);
     }
@@ -135,23 +202,72 @@
     @Test
     public void testClearVibrationParams_clearsCachedAdaptiveHapticsScales()
             throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-        mVibratorControlService.registerVibratorController(fakeController);
-        mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM, fakeController);
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+        int types = buildVibrationTypesMask(ScaleParam.TYPE_ALARM, ScaleParam.TYPE_NOTIFICATION);
 
-        verify(mMockVibrationScaler).updateAdaptiveHapticsScales(null);
+        mVibratorControlService.clearVibrationParams(types, mFakeVibratorController);
+
+        verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_ALARM);
+        verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_NOTIFICATION);
+        // Clearing ScaleParam.TYPE_NOTIFICATION will clear vibration scaling for both
+        // notification and communication request usages.
+        verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST);
     }
 
     @Test
     public void testClearVibrationParams_withUnregisteredController_ignoresRequest()
             throws RemoteException {
-        FakeVibratorController fakeController = new FakeVibratorController();
-
-        mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM, fakeController);
+        mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM,
+                mFakeVibratorController);
 
         verifyZeroInteractions(mMockVibrationScaler);
     }
 
+    @Test
+    public void testRequestVibrationParams_createsFutureRequestProperly()
+            throws RemoteException {
+        int timeoutInMillis = 10;
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+        CompletableFuture<Void> future =
+                mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+                        timeoutInMillis);
+        try {
+            future.orTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).get();
+        } catch (Throwable ignored) {
+        }
+        assertThat(mFakeVibratorController.didRequestVibrationParams).isTrue();
+        assertThat(mFakeVibratorController.requestVibrationType).isEqualTo(
+                ScaleParam.TYPE_RINGTONE);
+        assertThat(mFakeVibratorController.requestTimeoutInMillis).isEqualTo(timeoutInMillis);
+    }
+
+    @Test
+    public void testShouldRequestVibrationParams_returnsTrueForVibrationsThatShouldRequestParams()
+            throws RemoteException {
+        int[] vibrations =
+                new int[]{USAGE_ALARM, USAGE_RINGTONE, USAGE_MEDIA, USAGE_TOUCH, USAGE_NOTIFICATION,
+                        USAGE_HARDWARE_FEEDBACK, USAGE_UNKNOWN, USAGE_COMMUNICATION_REQUEST};
+        mVibratorControlService.registerVibratorController(mFakeVibratorController);
+
+        for (int vibration : vibrations) {
+            assertThat(mVibratorControlService.shouldRequestVibrationParams(vibration))
+                    .isEqualTo(ArrayUtils.contains(
+                            mVibrationSettings.getRequestVibrationParamsForUsages(), vibration));
+        }
+    }
+
+    @Test
+    public void testShouldRequestVibrationParams_unregisteredVibratorController_returnsFalse()
+            throws RemoteException {
+        int[] vibrations =
+                new int[]{USAGE_ALARM, USAGE_RINGTONE, USAGE_MEDIA, USAGE_TOUCH, USAGE_NOTIFICATION,
+                        USAGE_HARDWARE_FEEDBACK, USAGE_UNKNOWN, USAGE_COMMUNICATION_REQUEST};
+
+        for (int vibration : vibrations) {
+            assertThat(mVibratorControlService.shouldRequestVibrationParams(vibration)).isFalse();
+        }
+    }
+
     private VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) {
         List<VibrationParam> vibrationParamList = new ArrayList<>();
         for (int i = 0; i < vibrationScales.size(); i++) {
@@ -173,4 +289,12 @@
 
         return vibrationParam;
     }
+
+    private int buildVibrationTypesMask(int... types) {
+        int typesMask = 0;
+        for (int type : types) {
+            typesMask |= type;
+        }
+        return typesMask;
+    }
 }
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index d6b2116..bdbb6c6 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -318,6 +318,12 @@
                         return new HapticFeedbackVibrationProvider(
                                 resources, vibratorInfo, mHapticFeedbackVibrationMap);
                     }
+
+                    VibratorControllerHolder createVibratorControllerHolder() {
+                        VibratorControllerHolder holder = new VibratorControllerHolder();
+                        holder.setVibratorController(new FakeVibratorController());
+                        return holder;
+                    }
                 });
         return mService;
     }
@@ -965,8 +971,9 @@
                 new long[]{10, 10_000}, new int[]{255, 0}, 1);
         vibrate(service, repeatingEffect, ALARM_ATTRS);
 
-        // VibrationThread will start this vibration async, wait until the off waveform step.
-        assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+        // VibrationThread will start this vibration async, wait until it has started.
+        assertTrue(waitUntil(s -> !fakeVibrator.getAllEffectSegments().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
 
         vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
                 ALARM_ATTRS);
@@ -1019,8 +1026,9 @@
                 new long[]{10, 10_000}, new int[]{255, 0}, -1);
         vibrate(service, alarmEffect, ALARM_ATTRS);
 
-        // VibrationThread will start this vibration async, wait until the off waveform step.
-        assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+        // VibrationThread will start this vibration async, wait until it has started.
+        assertTrue(waitUntil(s -> !fakeVibrator.getAllEffectSegments().isEmpty(), service,
+                TEST_TIMEOUT_MILLIS));
 
         vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
                 UNKNOWN_ATTRS);
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
index 7e23587..3912206 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
@@ -20,6 +20,7 @@
 import android.frameworks.vibrator.IVibratorController;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.VibrationAttributes;
 
 /**
  * Provides a fake implementation of {@link android.frameworks.vibrator.IVibratorController} for
@@ -28,10 +29,16 @@
 public final class FakeVibratorController extends IVibratorController.Stub {
 
     public boolean isLinkedToDeath = false;
+    public boolean didRequestVibrationParams = false;
+    public int requestVibrationType = VibrationAttributes.USAGE_UNKNOWN;
+    public long requestTimeoutInMillis = 0;
 
     @Override
-    public void requestVibrationParams(int i, long l, IBinder iBinder) throws RemoteException {
-
+    public void requestVibrationParams(int vibrationType, long timeoutInMillis, IBinder iBinder)
+            throws RemoteException {
+        didRequestVibrationParams = true;
+        requestVibrationType = vibrationType;
+        requestTimeoutInMillis = timeoutInMillis;
     }
 
     @Override
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 0382ca0..e21388e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -34,6 +34,7 @@
 import static android.view.KeyEvent.KEYCODE_SLASH;
 import static android.view.KeyEvent.KEYCODE_SPACE;
 import static android.view.KeyEvent.KEYCODE_TAB;
+import static android.view.KeyEvent.KEYCODE_SCREENSHOT;
 import static android.view.KeyEvent.KEYCODE_U;
 import static android.view.KeyEvent.KEYCODE_Z;
 
@@ -193,4 +194,26 @@
             mPhoneWindowManager.verifyNewBrightness(newBrightness[i]);
         }
     }
+
+    /**
+     * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
+     */
+    @Test
+    public void testTakeScreenshot_flagEnabled() {
+        mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
+                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+        mPhoneWindowManager.assertTakeScreenshotCalled();
+    }
+
+    /**
+     * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
+     */
+    @Test
+    public void testTakeScreenshot_flagDisabled() {
+        mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
+                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+        mPhoneWindowManager.assertTakeScreenshotNotCalled();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 157d162..fee6582 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,6 +46,7 @@
 import static java.util.Collections.unmodifiableMap;
 
 import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
@@ -57,11 +58,17 @@
 
 import org.junit.After;
 import org.junit.Rule;
+import org.junit.rules.RuleChain;
 
 import java.util.Map;
 
 class ShortcutKeyTestBase {
-    @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    @Rule
+    public RuleChain rules = RuleChain.outerRule(mSettingsProviderRule).around(mSetFlagsRule);
 
     TestPhoneWindowManager mPhoneWindowManager;
     DispatchedKeyHandler mDispatchedKeyHandler = event -> false;
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index c8abd8d..2904c03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -596,6 +596,11 @@
         verify(mDisplayPolicy).takeScreenshot(anyInt(), anyInt());
     }
 
+    void assertTakeScreenshotNotCalled() {
+        mTestLooper.dispatchAll();
+        verify(mDisplayPolicy, never()).takeScreenshot(anyInt(), anyInt());
+    }
+
     void assertShowGlobalActionsCalled() {
         mTestLooper.dispatchAll();
         verify(mPhoneWindowManager).showGlobalActions();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 9e00f92..5d14334 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -417,6 +417,8 @@
                     di.logicalWidth, di.logicalHeight).mConfigInsets.top);
         }
 
+        // Flush the pending change (DecorInsets.Info#mNeedUpdate) for the rotation to be tested.
+        displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, di.logicalHeight, di.logicalWidth);
         // Add a window that provides the same insets in current rotation. But it specifies
         // different insets in other rotations.
         final WindowState bar2 = createWindow(null, navbar.mAttrs.type, "bar2");
@@ -446,6 +448,12 @@
         // The insets in other rotations should be still updated.
         assertEquals(doubleHeightFor90, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
                 di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
+        // Restore to previous height and the insets can still be updated.
+        bar2.mAttrs.paramsForRotation[Surface.ROTATION_90].providedInsets[0].setInsetsSize(
+                Insets.of(0, 0, 0, NAV_BAR_HEIGHT));
+        assertFalse(displayPolicy.updateDecorInsetsInfo());
+        assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
+                di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
 
         navbar.removeIfPossible();
         bar2.removeIfPossible();
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 3b4b220..9930c88 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -1207,6 +1208,29 @@
     }
 
     @Test
+    public void addTask_tasksAreAddedAccordingToZOrder() {
+        final Task firstTask = new TaskBuilder(mSupervisor).setTaskId(1)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        final Task secondTask = new TaskBuilder(mSupervisor).setTaskId(2)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+
+        assertEquals(-1, firstTask.compareTo(secondTask));
+
+        // initial addition when tasks are created
+        mRecentTasks.add(firstTask);
+        mRecentTasks.add(secondTask);
+
+        assertRecentTasksOrder(secondTask, firstTask);
+
+        // Tasks are added in a different order
+        mRecentTasks.add(secondTask);
+        mRecentTasks.add(firstTask);
+
+        // order in recents don't change as first task has lower z-order
+        assertRecentTasksOrder(secondTask, firstTask);
+    }
+
+    @Test
     public void removeTask_callsTaskNotificationController() {
         final Task task = createTaskBuilder(".Task").build();
 
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 1bf11df..eb7e67d 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1125,7 +1125,7 @@
     /**
      * TelephonyProvider column name for satellite attach enabled for carrier. The value of this
      * column is set based on user settings.
-     * By default, it's disabled.
+     * By default, it's enabled.
      * <P>Type: INTEGER (int)</P>
      * @hide
      */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index c1ceaef..89661a4 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6638,11 +6638,7 @@
         }
     }
 
-    /**
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    private ITelephony getITelephony() {
+    private static ITelephony getITelephony() {
         // Keeps cache disabled until test fixes are checked into AOSP.
         if (!sServiceHandleCacheEnabled) {
             return ITelephony.Stub.asInterface(
@@ -18696,11 +18692,7 @@
     @SimState
     public static int getSimStateForSlotIndex(int slotIndex) {
         try {
-            ITelephony telephony = ITelephony.Stub.asInterface(
-                    TelephonyFrameworkInitializer
-                            .getTelephonyServiceManager()
-                            .getTelephonyServiceRegisterer()
-                            .get());
+            ITelephony telephony = getITelephony();
             if (telephony != null) {
                 return telephony.getSimStateForSlotIndex(slotIndex);
             }
@@ -18970,11 +18962,11 @@
     @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
     @SystemApi
-    public void setEnableNullCipherNotifications(boolean enable) {
+    public void setNullCipherNotificationsEnabled(boolean enable) {
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                telephony.setEnableNullCipherNotifications(enable);
+                telephony.setNullCipherNotificationsEnabled(enable);
             } else {
                 throw new IllegalStateException("telephony service is null.");
             }
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 09d2108..9b8e62f 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -43,6 +43,7 @@
 import android.util.Log;
 
 import com.android.internal.telephony.euicc.IEuiccController;
+import com.android.internal.telephony.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -897,9 +898,7 @@
     /** @hide */
     public EuiccManager(Context context) {
         mContext = context;
-        TelephonyManager tm = (TelephonyManager)
-                context.getSystemService(Context.TELEPHONY_SERVICE);
-        mCardId = tm.getCardIdForDefaultEuicc();
+        mCardId = getCardIdForDefaultEuicc();
     }
 
     /** @hide */
@@ -1646,14 +1645,9 @@
     private boolean refreshCardIdIfUninitialized() {
         // Refresh mCardId if its UNINITIALIZED_CARD_ID
         if (mCardId == TelephonyManager.UNINITIALIZED_CARD_ID) {
-            TelephonyManager tm = (TelephonyManager)
-                    mContext.getSystemService(Context.TELEPHONY_SERVICE);
-            mCardId = tm.getCardIdForDefaultEuicc();
+            mCardId = getCardIdForDefaultEuicc();
         }
-        if (mCardId == TelephonyManager.UNINITIALIZED_CARD_ID) {
-            return false;
-        }
-        return true;
+        return mCardId != TelephonyManager.UNINITIALIZED_CARD_ID;
     }
 
     private static void sendUnavailableError(PendingIntent callbackIntent) {
@@ -1672,6 +1666,23 @@
                         .get());
     }
 
+    private int getCardIdForDefaultEuicc() {
+        int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
+
+        if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+            PackageManager pm = mContext.getPackageManager();
+            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
+                TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+                cardId = tm.getCardIdForDefaultEuicc();
+            }
+        } else {
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+            cardId = tm.getCardIdForDefaultEuicc();
+        }
+
+        return cardId;
+    }
+
     /**
      * Returns whether the passing portIndex is available.
      * A port is available if it is active without enabled profile on it or
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 43eb111..a1fc064 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3247,7 +3247,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.MODIFY_PHONE_STATE)")
-    void setEnableNullCipherNotifications(boolean enable);
+    void setNullCipherNotificationsEnabled(boolean enable);
 
     /**
      * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 566e51a..cbec85e 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -146,6 +146,7 @@
         verify(native).setTouchpadPointerSpeed(anyInt())
         verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean())
         verify(native).setTouchpadTapToClickEnabled(anyBoolean())
+        verify(native).setTouchpadTapDraggingEnabled(anyBoolean())
         verify(native).setTouchpadRightClickZoneEnabled(anyBoolean())
         verify(native).setShowTouches(anyBoolean())
         verify(native).setMotionClassifierEnabled(anyBoolean())
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 6015e931..381c574 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -101,7 +101,7 @@
     @Mock protected Context mContext;
     @Mock protected Network mNetwork;
     @Mock protected FeatureFlags mFeatureFlags;
-    @Mock protected com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+    @Mock protected android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags;
     @Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot;
     @Mock protected TelephonyManager mTelephonyManager;
     @Mock protected IPowerManager mPowerManagerService;
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 275a0e2..938a5ed 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -232,3 +232,39 @@
         ],
     },
 }
+
+cc_genrule {
+    name: "aapt2_results",
+    srcs: [
+        ":aapt2_tests",
+        "integration-tests/CompileTest/**/*",
+        "integration-tests/CommandTests/**/*",
+        "integration-tests/ConvertTest/**/*",
+        "integration-tests/DumpTest/**/*",
+    ],
+    host_supported: true,
+    device_supported: false,
+    target: {
+        windows: {
+            compile_multilib: "64",
+        },
+    },
+    out: ["result.xml"],
+    cmd: "mkdir -p $(genDir)/integration-tests/CompileTest/ && " +
+        "cp $(locations integration-tests/CompileTest/**/*) $(genDir)/integration-tests/CompileTest/ && " +
+        "mkdir -p $(genDir)/integration-tests/CommandTests/ && " +
+        "cp $(locations integration-tests/CommandTests/**/*) $(genDir)/integration-tests/CompileTest/ && " +
+        "mkdir -p $(genDir)/integration-tests/ConvertTest/ && " +
+        "cp $(locations integration-tests/ConvertTest/**/*) $(genDir)/integration-tests/ConvertTest/ && " +
+        "mkdir -p $(genDir)/integration-tests/DumpTest/ && " +
+        "cp $(locations integration-tests/DumpTest/**/*) $(genDir)/integration-tests/DumpTest/ && " +
+        "cp $(locations :aapt2_tests) $(genDir)/ && " +
+        "$(genDir)/aapt2_tests " +
+        "--gtest_output=xml:$(out) " +
+        ">/dev/null 2>&1 ; true",
+}
+
+phony_rule {
+    name: "aapt2_run_host_unit_tests",
+    phony_deps: ["aapt2_results"],
+}
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 34a1b11..15ae2ba 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -1,22 +1,4 @@
-LOCAL_PATH := $(call my-dir)
-
 include $(CLEAR_VARS)
-
-aapt2_results := $(call intermediates-dir-for,PACKAGING,aapt2_run_host_unit_tests)/result.xml
-
-# Target for running host unit tests on post/pre-submit.
-.PHONY: aapt2_run_host_unit_tests
-aapt2_run_host_unit_tests: $(aapt2_results)
-
-$(call dist-for-goals,aapt2_run_host_unit_tests,$(aapt2_results):gtest/aapt2_host_unit_tests_result.xml)
-
-# Always run the tests again, even if they haven't changed
-$(aapt2_results): .KATI_IMPLICIT_OUTPUTS := $(aapt2_results)-nocache
-$(aapt2_results): $(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests
-	-$(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests --gtest_output=xml:$@ > /dev/null 2>&1
-
+aapt2_results := ./out/soong/.intermediates/frameworks/base/tools/aapt2/aapt2_results
 $(call declare-1p-target,$(aapt2_results))
-
 aapt2_results :=
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
index 292e8da..6480cfc 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
@@ -15,9 +15,9 @@
  */
 package com.android.hoststubgen.nativesubstitution;
 
-import android.util.Log;
-import android.util.Log.Level;
+import com.android.internal.os.RuntimeInit;
 
+import java.io.PrintStream;
 import java.util.Collection;
 
 public class EventLog_host {
@@ -54,7 +54,7 @@
             }
         }
         sb.append(']');
-        System.out.println(sb.toString());
+        getRealOut().println(sb.toString());
         return sb.length();
     }
 
@@ -66,4 +66,16 @@
             Collection<android.util.EventLog.Event> output) {
         throw new UnsupportedOperationException();
     }
+
+    /**
+     * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
+     * that we don't end up in a recursive loop.
+     */
+    private static PrintStream getRealOut() {
+        if (RuntimeInit.sOut$ravenwood != null) {
+            return RuntimeInit.sOut$ravenwood;
+        } else {
+            return System.out;
+        }
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
index ee55c7a..cdfa302 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
@@ -18,6 +18,8 @@
 import android.util.Log;
 import android.util.Log.Level;
 
+import com.android.internal.os.RuntimeInit;
+
 import java.io.PrintStream;
 
 public class Log_host {
@@ -27,7 +29,6 @@
     }
 
     public static int println_native(int bufID, int priority, String tag, String msg) {
-        final PrintStream out = System.out;
         final String buffer;
         switch (bufID) {
             case Log.LOG_ID_MAIN: buffer = "main"; break;
@@ -50,7 +51,7 @@
         };
 
         for (String s : msg.split("\\n")) {
-            out.println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
+            getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
         }
         return msg.length();
     }
@@ -58,4 +59,16 @@
     public static int logger_entry_max_payload_native() {
         return 4068; // [ravenwood] This is what people use in various places.
     }
+
+    /**
+     * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
+     * that we don't end up in a recursive loop.
+     */
+    private static PrintStream getRealOut() {
+        if (RuntimeInit.sOut$ravenwood != null) {
+            return RuntimeInit.sOut$ravenwood;
+        } else {
+            return System.out;
+        }
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
index 2e47d48..65da4a1 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
@@ -28,6 +28,7 @@
 
     private final Object mPoller = new Object();
     private volatile boolean mPolling;
+    private volatile boolean mPendingWake;
 
     private void validate() {
         if (mDeleted) {
@@ -62,7 +63,9 @@
         synchronized (q.mPoller) {
             q.mPolling = true;
             try {
-                if (timeoutMillis == 0) {
+                if (q.mPendingWake) {
+                    // Calling with pending wake returns immediately
+                } else if (timeoutMillis == 0) {
                     // Calling epoll_wait() with 0 returns immediately
                 } else if (timeoutMillis == -1) {
                     q.mPoller.wait();
@@ -72,6 +75,8 @@
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             }
+            // Any reason for returning counts as a "wake", so clear pending
+            q.mPendingWake = false;
             q.mPolling = false;
         }
     }
@@ -79,6 +84,7 @@
     public static void nativeWake(long ptr) {
         var q = getInstance(ptr);
         synchronized (q.mPoller) {
+            q.mPendingWake = true;
             q.mPoller.notifyAll();
         }
     }
diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
index 7c6aa25..60eb47ee 100644
--- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
+++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
@@ -63,7 +63,10 @@
      */
     public static void onThrowMethodCalled() {
         // TODO: Maybe add call tracking?
-        throw new RuntimeException("This method is not supported on the host side");
+        throw new RuntimeException(
+                "This method is not yet supported under the Ravenwood deviceless testing "
+                        + "environment; consider requesting support from the API owner or "
+                        + "consider using Mockito; more details at go/ravenwood-docs");
     }
 
     /**
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index fc6b862..ba17c75 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -68,7 +68,7 @@
         TinyFrameworkForTextPolicy tfc = new TinyFrameworkForTextPolicy();
 
         thrown.expect(RuntimeException.class);
-        thrown.expectMessage("This method is not supported on the host side");
+        thrown.expectMessage("not yet supported");
         tfc.visibleButUsesUnsupportedMethod();
     }
 
@@ -182,7 +182,7 @@
 
         } catch (java.lang.reflect.InvocationTargetException e) {
             var inner = e.getCause();
-            assertThat(inner.getMessage()).contains("not supported on the host side");
+            assertThat(inner.getMessage()).contains("not yet supported");
         }
     }
 
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
index 20cc2ec..288c716 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
@@ -60,7 +60,7 @@
         TinyFrameworkClassAnnotations tfc = new TinyFrameworkClassAnnotations();
 
         thrown.expect(RuntimeException.class);
-        thrown.expectMessage("This method is not supported on the host side");
+        thrown.expectMessage("not yet supported");
         tfc.visibleButUsesUnsupportedMethod();
     }
 }
diff --git a/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java b/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
index 21700d5..6d57a87 100644
--- a/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
+++ b/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
@@ -16,11 +16,13 @@
 
 package android.net.wifi.nl80211;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.net.wifi.ScanResult;
 import android.net.wifi.WifiAnnotations.ChannelWidth;
 import android.net.wifi.WifiAnnotations.WifiStandard;
+import android.net.wifi.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
@@ -48,6 +50,7 @@
     private boolean mChannelWidth320MhzSupported;
     private int mMaxNumberTxSpatialStreams;
     private int mMaxNumberRxSpatialStreams;
+    private int mMaxNumberAkms;
 
 
     /** public constructor */
@@ -61,6 +64,7 @@
         mChannelWidth320MhzSupported = false;
         mMaxNumberTxSpatialStreams = 1;
         mMaxNumberRxSpatialStreams = 1;
+        mMaxNumberAkms = 1;
     }
 
     /**
@@ -199,6 +203,25 @@
     }
 
     /**
+     * Get the maximum number of AKM suites supported in the connection request to the driver.
+     *
+     * @return maximum number of AKMs
+     */
+    @FlaggedApi(Flags.FLAG_GET_DEVICE_CROSS_AKM_ROAMING_SUPPORT)
+    public int getMaxNumberAkms() {
+        return mMaxNumberAkms;
+    }
+
+    /**
+     * Set the maximum number of AKM suites supported in the connection request to the driver.
+     *
+     * @hide
+     */
+    public void setMaxNumberAkms(int akms) {
+        mMaxNumberAkms = akms;
+    }
+
+    /**
      * Set maximum number of receive spatial streams
      *
      * @param streams number of streams
@@ -226,7 +249,8 @@
                 && mChannelWidth80p80MhzSupported == capa.mChannelWidth80p80MhzSupported
                 && mChannelWidth320MhzSupported == capa.mChannelWidth320MhzSupported
                 && mMaxNumberTxSpatialStreams == capa.mMaxNumberTxSpatialStreams
-                && mMaxNumberRxSpatialStreams == capa.mMaxNumberRxSpatialStreams;
+                && mMaxNumberRxSpatialStreams == capa.mMaxNumberRxSpatialStreams
+                && mMaxNumberAkms == capa.mMaxNumberAkms;
     }
 
     /** override hash code */
@@ -235,7 +259,7 @@
         return Objects.hash(m80211nSupported, m80211acSupported, m80211axSupported,
                 m80211beSupported, mChannelWidth160MhzSupported, mChannelWidth80p80MhzSupported,
                 mChannelWidth320MhzSupported, mMaxNumberTxSpatialStreams,
-                mMaxNumberRxSpatialStreams);
+                mMaxNumberRxSpatialStreams, mMaxNumberAkms);
     }
 
     /** implement Parcelable interface */
@@ -259,6 +283,7 @@
         out.writeBoolean(mChannelWidth320MhzSupported);
         out.writeInt(mMaxNumberTxSpatialStreams);
         out.writeInt(mMaxNumberRxSpatialStreams);
+        out.writeInt(mMaxNumberAkms);
     }
 
     @Override
@@ -276,6 +301,7 @@
                 .append(mChannelWidth320MhzSupported ? "Yes" : "No");
         sb.append("mMaxNumberTxSpatialStreams: ").append(mMaxNumberTxSpatialStreams);
         sb.append("mMaxNumberRxSpatialStreams: ").append(mMaxNumberRxSpatialStreams);
+        sb.append("mMaxNumberAkms: ").append(mMaxNumberAkms);
 
         return sb.toString();
     }
@@ -298,6 +324,7 @@
             capabilities.mChannelWidth320MhzSupported = in.readBoolean();
             capabilities.mMaxNumberTxSpatialStreams = in.readInt();
             capabilities.mMaxNumberRxSpatialStreams = in.readInt();
+            capabilities.mMaxNumberAkms = in.readInt();
             return capabilities;
         }
 
diff --git a/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java b/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
index 7b900fe..5a3ca2b 100644
--- a/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
+++ b/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
@@ -49,6 +49,7 @@
         capa.setChannelWidthSupported(ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ, false);
         capa.setMaxNumberTxSpatialStreams(2);
         capa.setMaxNumberRxSpatialStreams(1);
+        capa.setMaxNumberAkms(2);
 
         Parcel parcel = Parcel.obtain();
         capa.writeToParcel(parcel, 0);
diff --git a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
index 362eb14..02da835 100644
--- a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
@@ -1130,6 +1130,7 @@
         capaExpected.setChannelWidthSupported(ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ, false);
         capaExpected.setMaxNumberTxSpatialStreams(2);
         capaExpected.setMaxNumberRxSpatialStreams(1);
+        capaExpected.setMaxNumberAkms(2);
 
         when(mWificond.getDeviceWiphyCapabilities(TEST_INTERFACE_NAME))
                 .thenReturn(capaExpected);
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
new file mode 100644
index 0000000..6ac986e
--- /dev/null
+++ b/wifi/wifi.aconfig
@@ -0,0 +1,9 @@
+package: "android.net.wifi.flags"
+
+flag {
+    name: "get_device_cross_akm_roaming_support"
+    namespace: "wifi"
+    description: "Add new API to get the device support for CROSS-AKM roaming"
+    bug: "313038031"
+    is_fixed_read_only: true
+}