Merge "Remove the unused `newModeToLegacyMode` from `KeyguardService`." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ee7ace..e6e835b 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -19,6 +19,7 @@
     ":android.app.flags-aconfig-java{.generated_srcjars}",
     ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}",
     ":android.app.usage.flags-aconfig-java{.generated_srcjars}",
+    ":android.app.wearable.flags-aconfig-java{.generated_srcjars}",
     ":android.appwidget.flags-aconfig-java{.generated_srcjars}",
     ":android.chre.flags-aconfig-java{.generated_srcjars}",
     ":android.companion.flags-aconfig-java{.generated_srcjars}",
@@ -1123,3 +1124,16 @@
     ],
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Wearable Sensing
+aconfig_declarations {
+    name: "android.app.wearable.flags-aconfig",
+    package: "android.app.wearable",
+    srcs: ["core/java/android/app/wearable/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.app.wearable.flags-aconfig-java",
+    aconfig_declarations: "android.app.wearable.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index 0a62877..aec2842 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -13,6 +13,7 @@
     field public static final String ACCESS_CHECKIN_PROPERTIES = "android.permission.ACCESS_CHECKIN_PROPERTIES";
     field public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
     field public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
+    field @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES = "android.permission.ACCESS_HIDDEN_PROFILES";
     field public static final String ACCESS_LOCATION_EXTRA_COMMANDS = "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS";
     field public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION";
     field public static final String ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE";
@@ -6355,6 +6356,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;
@@ -15573,6 +15575,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);
@@ -16221,6 +16224,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);
@@ -21174,6 +21195,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);
@@ -44523,6 +44545,7 @@
     method @Nullable public android.telephony.CellIdentity getCellIdentity();
     method public int getDomain();
     method @Nullable public String getRegisteredPlmn();
+    method @FlaggedApi("com.android.internal.telephony.flags.network_registration_info_reject_cause") public int getRejectCause();
     method public int getTransportType();
     method public boolean isNetworkRegistered();
     method public boolean isNetworkRoaming();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3b18dac..e6040f8 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -414,6 +414,7 @@
     field public static final String WRITE_OBB = "android.permission.WRITE_OBB";
     field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG";
     field public static final String WRITE_SMS = "android.permission.WRITE_SMS";
+    field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS";
   }
 
   public static final class Manifest.permission_group {
@@ -3157,6 +3158,7 @@
     field public static final int STATUS_SUCCESS = 1; // 0x1
     field public static final int STATUS_UNKNOWN = 0; // 0x0
     field public static final int STATUS_UNSUPPORTED = 2; // 0x2
+    field @FlaggedApi("android.app.wearable.enable_unsupported_operation_status_code") public static final int STATUS_UNSUPPORTED_OPERATION = 6; // 0x6
     field public static final int STATUS_WEARABLE_UNAVAILABLE = 4; // 0x4
   }
 
@@ -3209,6 +3211,11 @@
 
 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();
@@ -11298,6 +11305,12 @@
     field public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
+  @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+    method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
+    method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
+    method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int);
+  }
+
   @Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns {
     field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata";
     field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata";
@@ -14123,7 +14136,6 @@
     method @Nullable public android.telephony.DataSpecificRegistrationInfo getDataSpecificInfo();
     method public int getNetworkRegistrationState();
     method @Deprecated public int getRegistrationState();
-    method public int getRejectCause();
     method public int getRoamingType();
     method public boolean isEmergencyEnabled();
     method public void writeToParcel(android.os.Parcel, int);
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 5d2a26e..4c54b03 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -303,7 +303,7 @@
     public static final boolean DEBUG_MEMORY_TRIM = false;
     private static final boolean DEBUG_PROVIDER = false;
     public static final boolean DEBUG_ORDER = false;
-    private static final boolean DEBUG_APP_INFO = true;
+    private static final boolean DEBUG_APP_INFO = false;
     private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
     /**
      * The delay to release the provider when it has no more references. It reduces the number of
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 7907059..0dbce97 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1550,7 +1550,7 @@
             AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER;
 
     /**
-     * Allows an app whose primary use case is to backup or sync content to run longer jobs.
+     * Allows an app with a major use case of backing-up or syncing content to run longer jobs.
      *
      * @hide
      */
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/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index b21b0f3..ba9c895 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -1618,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
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index a271328..a045eae 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static android.app.Flags.enableNightModeCache;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
@@ -31,6 +33,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Binder;
+import android.os.IpcDataCache;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -874,6 +877,51 @@
         }
     }
 
+    private Integer getNightModeFromServer() {
+        try {
+            if (sGlobals != null) {
+                return sGlobals.mService.getNightMode();
+            }
+            return -1;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Retrieve the night mode for the user.
+     */
+    private final IpcDataCache.QueryHandler<Void, Integer> mNightModeQuery =
+            new IpcDataCache.QueryHandler<>() {
+
+                @Override
+                @NonNull
+                public Integer apply(Void query) {
+                    return getNightModeFromServer();
+                }
+            };
+
+    private static final String NIGHT_MODE_API = "getNightMode";
+
+    /**
+     * Cache the night mode for a user.
+     */
+    private final IpcDataCache<Void, Integer> mNightModeCache =
+            new IpcDataCache<>(1, IpcDataCache.MODULE_SYSTEM,
+                    NIGHT_MODE_API, /* cacheName= */ "NightModeCache", mNightModeQuery);
+
+    /**
+     * Invalidate the night mode cache.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_NIGHT_MODE_CACHE)
+    public static void invalidateNightModeCache() {
+        IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+                NIGHT_MODE_API);
+    }
+
     /**
      * Returns the currently configured night mode.
      * <p>
@@ -890,14 +938,11 @@
      * @see #setNightMode(int)
      */
     public @NightMode int getNightMode() {
-        if (sGlobals != null) {
-            try {
-                return sGlobals.mService.getNightMode();
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
+        if (enableNightModeCache()) {
+            return mNightModeCache.query(null);
+        } else {
+            return getNightModeFromServer();
         }
-        return -1;
     }
 
     /**
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/app/ui_mode_manager.aconfig b/core/java/android/app/ui_mode_manager.aconfig
new file mode 100644
index 0000000..1ae5264
--- /dev/null
+++ b/core/java/android/app/ui_mode_manager.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+     namespace: "system_performance"
+     name: "enable_night_mode_cache"
+     description: "Enables the use of binder caching for system night mode."
+     bug: "255999432"
+}
\ No newline at end of file
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index f1ca086..eca0039 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -99,6 +100,13 @@
      */
     public static final int STATUS_ACCESS_DENIED = 5;
 
+    /**
+     * The value of the status code that indicates the method called is not supported by the
+     * implementation of {@link WearableSensingService}.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE)
+    public static final int STATUS_UNSUPPORTED_OPERATION = 6;
+
     /** @hide */
     @IntDef(prefix = { "STATUS_" }, value = {
             STATUS_UNKNOWN,
@@ -106,7 +114,8 @@
             STATUS_UNSUPPORTED,
             STATUS_SERVICE_UNAVAILABLE,
             STATUS_WEARABLE_UNAVAILABLE,
-            STATUS_ACCESS_DENIED
+            STATUS_ACCESS_DENIED,
+            STATUS_UNSUPPORTED_OPERATION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface StatusCode {}
diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig
new file mode 100644
index 0000000..074ce9b
--- /dev/null
+++ b/core/java/android/app/wearable/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.app.wearable"
+
+flag {
+    name: "enable_unsupported_operation_status_code"
+    namespace: "machine_learning"
+    description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API."
+    bug: "301427767"
+}
\ No newline at end of file
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/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index bde562d..af13011 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -409,9 +409,10 @@
                     "packageName = " + mAttributionSourceState.packageName + ", " +
                     "attributionTag = " + mAttributionSourceState.attributionTag + ", " +
                     "token = " + mAttributionSourceState.token + ", " +
+                    "deviceId = " + mAttributionSourceState.deviceId + ", " +
                     "next = " + (mAttributionSourceState.next != null
-                                    && mAttributionSourceState.next.length > 0
-                            ? mAttributionSourceState.next[0] : null) +
+                    && mAttributionSourceState.next.length > 0
+                    ? new AttributionSource(mAttributionSourceState.next[0]).toString() : null) +
                     " }";
         }
         return super.toString();
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index caff457..19bce0b 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -183,3 +183,19 @@
     description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES"
     bug: "321080601"
 }
+
+flag {
+    name: "asl_in_apk_app_metadata_source"
+    namespace: "package_manager_service"
+    description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer."
+    bug: "287487923"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "force_multi_arch_native_libs_match"
+    namespace: "package_manager_service"
+    description: "Feature flag to force an multiArch app's native libraries to match with the natively supported ABIs of the device"
+    bug: "282783453"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index efb8607..d7e64b6 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -108,3 +108,17 @@
     bug: "316362775"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_permission_to_access_hidden_profiles"
+    namespace: "profile_experiences"
+    description: "Add permission to access API hidden users data via system APIs"
+    bug: "321988638"
+}
+
+flag {
+    name: "handle_interleaved_settings_for_private_space"
+    namespace: "profile_experiences"
+    description: "Handle listing of private space apps in settings pages with interleaved content"
+    bug: "323212460"
+}
diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java
index bd74b0b..a4db733 100644
--- a/core/java/android/content/pm/overlay/OverlayPaths.java
+++ b/core/java/android/content/pm/overlay/OverlayPaths.java
@@ -49,13 +49,6 @@
     public static class Builder {
         final OverlayPaths mPaths = new OverlayPaths();
 
-        public Builder() {}
-
-        public Builder(@NonNull OverlayPaths base) {
-            mPaths.mResourceDirs.addAll(base.getResourceDirs());
-            mPaths.mOverlayPaths.addAll(base.getOverlayPaths());
-        }
-
         /**
          * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}.
          */
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index c7790bd..5e442b8 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -273,14 +273,27 @@
         throw new NotFoundException("String resource name " + name);
     }
 
+    private static boolean isIntLike(@NonNull String s) {
+        if (s.isEmpty() || s.length() > 10) return false;
+        for (int i = 0, size = s.length(); i < size; i++) {
+            final char c = s.charAt(i);
+            if (c < '0' || c > '9') {
+                return false;
+            }
+        }
+        return true;
+    }
+
     int getIdentifier(String name, String defType, String defPackage) {
         if (name == null) {
             throw new NullPointerException("name is null");
         }
-        try {
-            return Integer.parseInt(name);
-        } catch (Exception e) {
-            // Ignore
+        if (isIntLike(name)) {
+            try {
+                return Integer.parseInt(name);
+            } catch (Exception e) {
+                // Ignore
+            }
         }
         return mAssets.getResourceIdentifier(name, defType, defPackage);
     }
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 90cd471..1ca11e6 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -47,4 +47,11 @@
     name: "configurable_selector_ui_enabled"
     description: "Enables OEM configurable Credential Selector UI"
     bug: "319448437"
-}
\ No newline at end of file
+}
+
+flag {
+    namespace: "credential_manager"
+    name: "credman_biometric_api_enabled"
+    description: "Enables Credential Manager to work with the Biometric Authenticate API"
+    bug: "323211850"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 64a62a9..f18a0b7 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -202,8 +202,11 @@
     /**
      * Called by the window manager to perform traversals while holding a
      * surface flinger transaction.
+     * @param t The default transaction.
+     * @param displayTransactions The transactions mapped by display id.
      */
-    public abstract void performTraversal(Transaction t);
+    public abstract void performTraversal(Transaction t,
+            SparseArray<SurfaceControl.Transaction> displayTransactions);
 
     /**
      * Tells the display manager about properties of the display that depend on the windows on it.
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/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java
index ecde699..bef6456 100644
--- a/core/java/android/provider/ContactKeysManager.java
+++ b/core/java/android/provider/ContactKeysManager.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -247,6 +248,44 @@
     }
 
     /**
+     * Updates a contact key entry's local verification state that belongs to the app identified
+     * by ownerPackageName.
+     *
+     * @param lookupKey the value that references the contact
+     * @param deviceId an app-specified identifier for the device
+     * @param accountId an app-specified identifier for the account
+     * @param ownerPackageName the package name of the app that owns the key
+     * @param localVerificationState the new local verification state
+     *
+     * @return true if the entry was updated, false otherwise.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS,
+            android.Manifest.permission.WRITE_CONTACTS})
+    public boolean updateContactKeyLocalVerificationState(@NonNull String lookupKey,
+            @NonNull String deviceId,
+            @NonNull String accountId,
+            @NonNull String ownerPackageName,
+            @VerificationState int localVerificationState) {
+        validateVerificationState(localVerificationState);
+
+        final Bundle extras = new Bundle();
+        extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey));
+        extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId));
+        extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId));
+        extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName));
+        extras.putInt(ContactKeys.LOCAL_VERIFICATION_STATE, localVerificationState);
+
+        final Bundle response = nullSafeCall(mContentResolver,
+                ContactKeys.UPDATE_CONTACT_KEY_LOCAL_VERIFICATION_STATE_METHOD, extras);
+
+        return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS);
+    }
+
+    /**
      * Updates a contact key entry's remote verification state that belongs to the caller app.
      *
      * @param lookupKey the value that references the contact
@@ -275,6 +314,45 @@
         return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS);
     }
 
+    /**
+     * Updates a contact key entry's remote verification state that belongs to the app identified
+     * by ownerPackageName.
+     *
+     * @param lookupKey the value that references the contact
+     * @param deviceId an app-specified identifier for the device
+     * @param accountId an app-specified identifier for the account
+     * @param ownerPackageName the package name of the app that owns the key
+     * @param remoteVerificationState the new remote verification state
+     *
+     * @return true if the entry was updated, false otherwise.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS,
+            android.Manifest.permission.WRITE_CONTACTS})
+    public boolean updateContactKeyRemoteVerificationState(@NonNull String lookupKey,
+            @NonNull String deviceId,
+            @NonNull String accountId,
+            @NonNull String ownerPackageName,
+            @VerificationState int remoteVerificationState) {
+        validateVerificationState(remoteVerificationState);
+
+        final Bundle extras = new Bundle();
+        extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey));
+        extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId));
+        extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId));
+        extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName));
+        extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState);
+
+        final Bundle response = nullSafeCall(mContentResolver,
+                ContactKeys.UPDATE_CONTACT_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras);
+
+        return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS);
+    }
+
+
     private static void validateVerificationState(int verificationState) {
         if (verificationState != UNVERIFIED
                 && verificationState != VERIFICATION_FAILED
@@ -297,12 +375,12 @@
     public boolean removeContactKey(@NonNull String lookupKey,
             @NonNull String deviceId,
             @NonNull String accountId) {
-        Bundle extras = new Bundle();
+        final Bundle extras = new Bundle();
         extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey));
         extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId));
         extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId));
 
-        Bundle response = nullSafeCall(mContentResolver,
+        final Bundle response = nullSafeCall(mContentResolver,
                 ContactKeys.REMOVE_CONTACT_KEY_METHOD, extras);
 
         return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS);
@@ -369,6 +447,41 @@
     }
 
     /**
+     * Updates a self key entry's remote verification state that belongs to the app identified
+     * by ownerPackageName.
+     *
+     * @param deviceId an app-specified identifier for the device
+     * @param accountId an app-specified identifier for the account
+     * @param ownerPackageName the package name of the app that owns the key
+     * @param remoteVerificationState the new remote verification state
+     *
+     * @return true if the entry was updated, false otherwise.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS,
+            android.Manifest.permission.WRITE_CONTACTS})
+    public boolean updateSelfKeyRemoteVerificationState(@NonNull String deviceId,
+            @NonNull String accountId,
+            @NonNull String ownerPackageName,
+            @VerificationState int remoteVerificationState) {
+        validateVerificationState(remoteVerificationState);
+
+        Bundle extras = new Bundle();
+        extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId));
+        extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId));
+        extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName));
+        extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState);
+
+        Bundle response = nullSafeCall(mContentResolver,
+                ContactKeys.UPDATE_SELF_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras);
+
+        return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS);
+    }
+
+    /**
      * Maximum size of a contact key.
      */
     public static int getMaxKeySizeBytes() {
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/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 064bc69..35b137a 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -314,13 +314,20 @@
         if (count < 2) {
             return null;
         }
+        View next = null;
+        final boolean[] looped = new boolean[1];
         switch (direction) {
             case View.FOCUS_FORWARD:
-                return getNextFocusable(focused, focusables, count);
+                next = getNextFocusable(focused, focusables, count, looped);
+                break;
             case View.FOCUS_BACKWARD:
-                return getPreviousFocusable(focused, focusables, count);
+                next = getPreviousFocusable(focused, focusables, count, looped);
+                break;
         }
-        return focusables.get(count - 1);
+        if (root != null && root.mAttachInfo != null && root == root.getRootView()) {
+            root.mAttachInfo.mNextFocusLooped = looped[0];
+        }
+        return next != null ? next : focusables.get(count - 1);
     }
 
     private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
@@ -375,7 +382,8 @@
         return closest;
     }
 
-    private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
+    private static View getNextFocusable(View focused, ArrayList<View> focusables, int count,
+            boolean[] outLooped) {
         if (count < 2) {
             return null;
         }
@@ -385,10 +393,12 @@
                 return focusables.get(position + 1);
             }
         }
+        outLooped[0] = true;
         return focusables.get(0);
     }
 
-    private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
+    private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count,
+            boolean[] outLooped) {
         if (count < 2) {
             return null;
         }
@@ -398,6 +408,7 @@
                 return focusables.get(position - 1);
             }
         }
+        outLooped[0] = true;
         return focusables.get(count - 1);
     }
 
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 1b22fda..0d2c2cc 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -31336,6 +31336,13 @@
         final ArrayList<View> mTempArrayList = new ArrayList<View>(24);
 
         /**
+         * Indicates if the next focus will be looped back to the first focusable view of the entire
+         * hierarchy when finding in the direction of {@link #FOCUS_FORWARD} or to the last
+         * focusable view when finding in the direction of {@link #FOCUS_BACKWARD}.
+         */
+        boolean mNextFocusLooped = false;
+
+        /**
          * The id of the window for accessibility purposes.
          */
         int mAccessibilityWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c18aeee..ae9c109 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -7283,8 +7283,18 @@
             if (direction != 0) {
                 View focused = mView.findFocus();
                 if (focused != null) {
+                    mAttachInfo.mNextFocusLooped = false;
                     View v = focused.focusSearch(direction);
                     if (v != null && v != focused) {
+                        if (mAttachInfo.mNextFocusLooped) {
+                            // The next focus is looped. Let's try to move the focus to the adjacent
+                            // window. Note: we still need to move the focus in this window
+                            // regardless of what moveFocusToAdjacentWindow returns, so the focus
+                            // can be looped back from the focus in the adjacent window to next
+                            // focus of this window.
+                            moveFocusToAdjacentWindow(direction);
+                        }
+
                         // do the math the get the interesting rect
                         // of previous focused into the coord system of
                         // newly focused view
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index ef1bf5a..146b576 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -2424,7 +2424,7 @@
             }
         }
         try {
-            service.attachAccessibilityOverlayToDisplay_enforcePermission(
+            service.attachAccessibilityOverlayToDisplay(
                     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 1c5d29e..eca1586 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -139,5 +139,5 @@
     WindowTransformationSpec getWindowTransformationSpec(int windowId);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
-    void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl);
+    void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl surfaceControl);
 }
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
index c6271d2..2f765ae 100644
--- a/core/java/android/webkit/URLUtil.java
+++ b/core/java/android/webkit/URLUtil.java
@@ -16,7 +16,6 @@
 
 package android.webkit;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.Compatibility;
@@ -47,8 +46,7 @@
      */
     @ChangeId
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM)
-    public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L;
+    static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L;
 
     private static final String LOGTAG = "webkit";
     private static final boolean TRACE = false;
diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig
new file mode 100644
index 0000000..9f0b7c3
--- /dev/null
+++ b/core/java/android/widget/flags/notification_widget_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.widget.flags"
+
+flag {
+   name: "notif_linearlayout_optimized"
+   namespace: "systemui"
+   description: "Enables notification specific LinearLayout optimization"
+   bug: "316110233"
+}
\ No newline at end of file
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/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0171f58..21d2bf2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -881,6 +881,19 @@
         android:description="@string/permdesc_writeContacts"
       android:protectionLevel="dangerous" />
 
+    <!-- Allows an app to update the verification status of E2EE contact keys owned by other apps.
+        <p>This permission is only granted to system apps.
+        <p>Protection level: signature|privileged
+        @SystemApi
+        @hide
+        @FlaggedApi("android.provider.user_keys")
+    -->
+    <permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_writeVerificationStateE2eeContactKeys"
+                android:description="@string/permdesc_writeVerificationStateE2eeContactKeys"
+                android:protectionLevel="signature|privileged" />
+
     <!-- Allows an application to set default account for new contacts.
         <p> This permission is only granted to system applications fulfilling the Contacts app role.
         <p>Protection level: internal|role
@@ -3188,6 +3201,14 @@
     <permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
         android:protectionLevel="signature|appop" />
 
+    <!-- Allows applications to access profiles with ACCESS_HIDDEN_PROFILES user property
+     <p>Protection level: normal
+     @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
+    <permission android:name="android.permission.ACCESS_HIDDEN_PROFILES"
+        android:label="@string/permlab_accessHiddenProfile"
+        android:description="@string/permdesc_accessHiddenProfile"
+        android:protectionLevel="normal" />
+
     <!-- @SystemApi @hide Allows starting activities across profiles in the same profile group. -->
     <permission android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES"
                 android:protectionLevel="signature|role" />
@@ -7828,7 +7849,7 @@
                 android:protectionLevel="normal"/>
 
     <!-- @FlaggedApi("android.app.job.backup_jobs_exemption")
-         Gives applications whose <b>primary use case</b> is to backup or sync content increased
+         Gives applications with a <b>major use case</b> of backing-up or syncing content increased
          job execution allowance in order to complete the related work. The jobs must have a valid
          content URI trigger and network constraint set.
          <p>This is a special access permission that can be revoked by the system or the user.
diff --git a/core/res/res/drawable/ic_satellite_alt_24px.xml b/core/res/res/drawable/ic_satellite_alt_24px.xml
new file mode 100644
index 0000000..f9ca7dc
--- /dev/null
+++ b/core/res/res/drawable/ic_satellite_alt_24px.xml
@@ -0,0 +1,25 @@
+<!--
+     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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?android:attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M560,928L560,848Q677,848 758.5,766.5Q840,685 840,568L920,568Q920,643 891.5,708.5Q863,774 814.5,822.5Q766,871 700.5,899.5Q635,928 560,928ZM560,768L560,688Q610,688 645,653Q680,618 680,568L760,568Q760,651 701.5,709.5Q643,768 560,768ZM222,903Q207,903 192,897Q177,891 165,880L23,738Q12,726 6,711Q0,696 0,681Q0,665 6,650.5Q12,636 23,625L150,498Q173,475 207,474.5Q241,474 264,497L314,547L342,519L292,469Q269,446 269,413Q269,380 292,357L349,300Q372,277 405.5,277Q439,277 462,300L512,350L540,322L490,272Q467,249 467,215.5Q467,182 490,159L617,32Q629,20 644,14Q659,8 674,8Q689,8 703.5,14Q718,20 730,32L872,174Q884,185 889.5,199.5Q895,214 895,230Q895,245 889.5,260Q884,275 872,287L745,414Q722,437 688.5,437Q655,437 632,414L582,364L554,392L604,442Q627,465 626.5,498.5Q626,532 603,555L547,611Q524,634 490.5,634Q457,634 434,611L384,561L356,589L406,639Q429,662 428.5,696Q428,730 405,753L278,880Q267,891 252.5,897Q238,903 222,903ZM222,824Q222,824 222,824Q222,824 222,824L264,782L122,640L80,682Q80,682 80,682Q80,682 80,682L222,824ZM307,739L349,697Q349,697 349,697Q349,697 349,697L207,555Q207,555 207,555Q207,555 207,555L165,597L307,739ZM491,555Q491,555 491,555Q491,555 491,555L547,499Q547,499 547,499Q547,499 547,499L405,357Q405,357 405,357Q405,357 405,357L349,413Q349,413 349,413Q349,413 349,413L491,555ZM689,357Q689,357 689,357Q689,357 689,357L731,315L589,173L547,215Q547,215 547,215Q547,215 547,215L689,357ZM774,272L816,230Q816,230 816,230Q816,230 816,230L674,88Q674,88 674,88Q674,88 674,88L632,130L774,272ZM448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456Z"/>
+</vector>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 20a755f..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. -->
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 558bae7..ec47313 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1572,6 +1572,11 @@
     <string name="permdesc_setWallpaper">Allows the app to set the system wallpaper.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_accessHiddenProfile">Access hidden profiles</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_accessHiddenProfile">Allows the app to access hidden profiles.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_setWallpaperHints">adjust your wallpaper size</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_setWallpaperHints">Allows the app to set the system wallpaper size hints.</string>
@@ -2222,6 +2227,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] -->
     <string name="permdesc_updatePackagesWithoutUserAction">Allows the holder to update the app it previously installed without user action</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] -->
+    <string name="permlab_writeVerificationStateE2eeContactKeys">update the verification states of E2EE contact keys owned by other apps</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] -->
+    <string name="permdesc_writeVerificationStateE2eeContactKeys">Allows the app to update the verification states of E2EE contact keys owned by other apps</string>
+
     <!-- Policy administration -->
 
     <!-- Title of policy access to limiting the user's password choices -->
@@ -6386,4 +6396,14 @@
     <string name="redacted_notification_message"></string>
     <!-- Notification action title used instead of a notification's normal title sensitive [CHAR_LIMIT=NOTIF_BODY] -->
     <string name="redacted_notification_action_title"></string>
+
+    <!-- Satellite related messages -->
+    <!-- Notification title when satellite service is connected. -->
+    <string name="satellite_notification_title">Auto connected to satellite</string>
+    <!-- Notification summary when satellite service is connected. [CHAR LIMIT=NONE] -->
+    <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string>
+    <!-- Invoke "What to expect" dialog of messaging application -->
+    <string name="satellite_notification_open_message">Open Messages</string>
+    <!-- Invoke Satellite setting activity of Settings -->
+    <string name="satellite_notification_how_it_works">How it works</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e57efdf..699c8ac 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" />
@@ -5327,4 +5329,11 @@
   <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" />
 
   <java-symbol type="integer" name="config_minMillisBetweenInputUserActivityEvents" />
+
+  <!-- System notification for satellite service -->
+  <java-symbol type="string" name="satellite_notification_title" />
+  <java-symbol type="string" name="satellite_notification_summary" />
+  <java-symbol type="string" name="satellite_notification_open_message" />
+  <java-symbol type="string" name="satellite_notification_how_it_works" />
+  <java-symbol type="drawable" name="ic_satellite_alt_24px" />
 </resources>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index f476799..513e022 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -203,7 +203,6 @@
         "androidx.test.uiautomator_uiautomator",
         "compatibility-device-util-axt",
         "flag-junit",
-        "mockito_ravenwood",
         "platform-test-annotations",
         "flag-junit",
         "testng",
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index 2ccee71..f5563a7 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -60,7 +60,6 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.test.rules",
-        "mockito_ravenwood",
         "frameworks-base-testutils",
         "servicestests-utils",
     ],
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 2873428..d4c2c2b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -571,6 +571,8 @@
         <!-- Permission required for BinaryTransparencyService shell API and host test -->
         <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" />
         <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
+        <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
+        <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
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/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/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 2b95f30..9a66c0f 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -57,3 +57,10 @@
     description: "Enables new UMO experience for PiP menu"
     bug: "307998712"
 }
+
+flag {
+    name: "enable_bubble_bar"
+    namespace: "multitasking"
+    description: "Enables the new bubble bar UI for tablets"
+    bug: "286246694"
+}
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 f32f030..50a58da 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
@@ -465,7 +465,7 @@
     /**
      * Call when all the views should be removed/cleaned up.
      */
-    void cleanupViews() {
+    public void cleanupViews() {
         cleanupViews(true);
     }
 
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 1a6bf28..d0db708 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
@@ -730,7 +730,7 @@
             // window to show this in, but we use a separate code path.
             // TODO(b/273312602): consider foldables where we do need a stack view when folded
             if (mLayerView == null) {
-                mLayerView = new BubbleBarLayerView(mContext, this);
+                mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData);
                 mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
             }
         } else {
@@ -1276,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);
         }
@@ -1706,8 +1714,7 @@
         @Override
         public void removeBubble(Bubble removedBubble) {
             if (mLayerView != null) {
-                // TODO: need to check if there's something that needs to happen here, e.g. if
-                //  the currently selected & expanded bubble is removed?
+                mLayerView.removeBubble(removedBubble);
             }
         }
 
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..dbfa260 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
@@ -15,7 +15,6 @@
  */
 package com.android.wm.shell.bubbles;
 
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
@@ -41,6 +40,7 @@
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.Bubbles.DismissReason;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.common.bubbles.RemovedBubble;
 
@@ -427,7 +427,7 @@
     /**
      * When this method is called it is expected that all info in the bubble has completed loading.
      * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView,
-     * BubbleIconFactory, boolean)
+     * BubbleBarLayerView, BubbleIconFactory, boolean)
      */
     void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
         if (DEBUG_BUBBLE_DATA) {
@@ -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.
@@ -1056,7 +1069,6 @@
     /**
      * The set of bubbles in row.
      */
-    @VisibleForTesting(visibility = PACKAGE)
     public List<Bubble> getBubbles() {
         return Collections.unmodifiableList(mBubbles);
     }
@@ -1145,7 +1157,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/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5fc67d7..dc27133 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
@@ -31,6 +31,7 @@
 import android.graphics.Rect;
 import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 
 import androidx.annotation.Nullable;
 
@@ -186,6 +187,7 @@
             }
             if (mTaskView != null) {
                 mTaskView.release();
+                ((ViewGroup) mParentView).removeView(mTaskView);
                 mTaskView = null;
             }
         }
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/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 00d683e..73a9cf4 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
@@ -266,13 +266,8 @@
         mListener.onBackPressed();
     }
 
-    /** Cleans up task view, should be called when the bubble is no longer active. */
+    /** Cleans up the expanded view, should be called when the bubble is no longer active. */
     public void cleanUpExpandedState() {
-        if (mBubbleTaskViewHelper != null) {
-            if (mTaskView != null) {
-                removeView(mTaskView);
-            }
-        }
         mMenuViewController.hideMenu(false /* animated */);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index bd8ce80..b95d258 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -34,7 +34,9 @@
 import android.widget.FrameLayout;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
 import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleData;
 import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
@@ -61,6 +63,7 @@
     private static final float SCRIM_ALPHA = 0.2f;
 
     private final BubbleController mBubbleController;
+    private final BubbleData mBubbleData;
     private final BubblePositioner mPositioner;
     private final BubbleBarAnimationHelper mAnimationHelper;
     private final BubbleEducationViewController mEducationViewController;
@@ -85,9 +88,10 @@
     private TouchDelegate mHandleTouchDelegate;
     private final Rect mHandleTouchBounds = new Rect();
 
-    public BubbleBarLayerView(Context context, BubbleController controller) {
+    public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) {
         super(context);
         mBubbleController = controller;
+        mBubbleData = bubbleData;
         mPositioner = mBubbleController.getPositioner();
 
         mAnimationHelper = new BubbleBarAnimationHelper(context,
@@ -236,15 +240,44 @@
         showScrim(true);
     }
 
+    /** Removes the given {@code bubble}. */
+    public void removeBubble(Bubble bubble) {
+        if (mBubbleData.getBubbles().isEmpty()) {
+            // we're removing the last bubble. collapse the expanded view and cleanup bubble views
+            // at the end.
+            collapse(bubble::cleanupViews);
+        } else {
+            bubble.cleanupViews();
+        }
+    }
+
     /** Collapses any showing expanded view */
     public void collapse() {
+        collapse(/* endAction= */ null);
+    }
+
+    /**
+     * Collapses any showing expanded view.
+     *
+     * @param endAction an action to run and the end of the collapse animation.
+     */
+    public void collapse(@Nullable Runnable endAction) {
+        if (!mIsExpanded) {
+            return;
+        }
         mIsExpanded = false;
         final BubbleBarExpandedView viewToRemove = mExpandedView;
         mEducationViewController.hideEducation(/* animated = */ true);
+        Runnable runnable = () -> {
+            removeView(viewToRemove);
+            if (endAction != null) {
+                endAction.run();
+            }
+        };
         if (mDragController != null && mDragController.isStuckToDismiss()) {
-            mAnimationHelper.animateDismiss(() -> removeView(viewToRemove));
+            mAnimationHelper.animateDismiss(runnable);
         } else {
-            mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+            mAnimationHelper.animateCollapse(runnable);
         }
         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
         mExpandedView = null;
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/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
new file mode 100644
index 0000000..fd91ac0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.sysui.ShellCommandHandler
+import java.io.PrintWriter
+
+/**
+ * Handles the shell commands for the DesktopTasksController.
+ */
+class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
+    ShellCommandHandler.ShellCommandActionHandler {
+
+    override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean {
+        return when (args[0]) {
+            "moveToDesktop" -> {
+                if (!runMoveToDesktop(args, pw)) {
+                    pw.println("Task not found. Please enter a valid taskId.")
+                    false
+                } else {
+                    true
+                }
+            }
+
+            else -> {
+                pw.println("Invalid command: ${args[0]}")
+                false
+            }
+        }
+    }
+
+    private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean {
+        if (args.size < 2) {
+            // First argument is the action name.
+            pw.println("Error: task id should be provided as arguments")
+            return false
+        }
+
+        val taskId = try {
+            args[1].toInt()
+        } catch (e: NumberFormatException) {
+            pw.println("Error: task id should be an integer")
+            return false
+        }
+
+        return controller.moveToDesktopWithoutDecor(taskId, WindowContainerTransaction())
+    }
+
+    override fun printShellCommandHelp(pw: PrintWriter, prefix: String) {
+        pw.println("$prefix moveToDesktop <taskId> ")
+        pw.println("$prefix  Move a task with given id to desktop mode.")
+    }
+}
\ No newline at end of file
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 a089e81..e872849 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
@@ -100,6 +100,9 @@
 
     private val desktopMode: DesktopModeImpl
     private var visualIndicator: DesktopModeVisualIndicator? = null
+    private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
+        DesktopModeShellCommandHandler(this)
+
     private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
         t: SurfaceControl.Transaction ->
         visualIndicator?.releaseVisualIndicator(t)
@@ -148,6 +151,8 @@
     private fun onInit() {
         KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
         shellCommandHandler.addDumpCallback(this::dump, this)
+        shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler,
+            this)
         shellController.addExternalInterface(
             ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
             { createExternalInterface() },
@@ -240,6 +245,40 @@
         }
     }
 
+    /** Move a task with given `taskId` to desktop without decor */
+    fun moveToDesktopWithoutDecor(
+        taskId: Int,
+        wct: WindowContainerTransaction
+    ): Boolean {
+        val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false
+        moveToDesktopWithoutDecor(task, wct)
+        return true
+    }
+
+    /**
+     * Move a task to desktop without decor
+     */
+    private fun moveToDesktopWithoutDecor(
+        task: RunningTaskInfo,
+        wct: WindowContainerTransaction
+    ) {
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: moveToDesktopWithoutDecor taskId=%d",
+            task.taskId
+        )
+        exitSplitIfApplicable(wct, task)
+        // Bring other apps to front first
+        bringDesktopAppsToFront(task.displayId, wct)
+        addMoveToDesktopChanges(wct, task)
+
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+        } else {
+            shellTaskOrganizer.applyTransaction(wct)
+        }
+    }
+
     /**
      * Move a task to desktop
      */
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 731fec7..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
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
index 73a7348..3fad28a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
@@ -15,4 +15,4 @@
 Todo
 - Per-feature docs
 - Feature flagging
-- Best practices
\ No newline at end of file
+- Best practices & patterns
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
index fbf326e..9aa5f4f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -102,5 +102,5 @@
 Launcher uses.
 
 If the new code doesn't fall into those categories, they can be added explicitly in the Shell's
-[Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) file under the
+[Android.bp](/libs/WindowManager/Shell/Android.bp) file under the
 `wm_shell_util-sources` filegroup.
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
index 6c01d96..7070dea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
@@ -21,16 +21,16 @@
 (especially as products override components).
 
 The module dependency tree looks a bit like:
-- [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java)
+- [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java)
   (provides threading-related components)
-  - [WMShellBaseModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java)
+  - [WMShellBaseModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java)
     (provides components that are likely common to all products, ie. DisplayController,
     Transactions, etc.)
-    - [WMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java)
+    - [WMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java)
       (phone/tablet specific components only)
-    - [TvPipModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java)
+    - [TvPipModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java)
       (PIP specific components for TV)
-      - [TvWMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java)
+      - [TvWMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java)
         (TV specific components only)
   - etc.
 
@@ -43,7 +43,7 @@
 product it runs on.  If there are hooks that can be added to the component, that is the
 preferable approach.
 
-The alternative is to use the [@DynamicOverride](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java)
+The alternative is to use the [@DynamicOverride](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java)
 annotation to allow the product module to provide an implementation that the base module can
 reference.  This is most useful if the existence of the entire component is controlled by the
 product and the override implementation is optional (there is a default implementation).  More
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index f9ea1d4..438aa76 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -29,30 +29,78 @@
 ### Kotlin
 
 Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)).
-For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt)
+For logging in Kotlin, use the [KtProtoLog](/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt)
 class which has a similar API to the Java ProtoLog class.
 
 ### Enabling ProtoLog command line logging
-Run these commands to enable protologs for both WM Core and WM Shell to print to logcat.
+Run these commands to enable protologs (in logcat) for WM Core ([list of all core tags](/core/java/com/android/internal/protolog/ProtoLogGroup.java)):
 ```shell
-adb shell wm logging enable-text NEW_FEATURE
-adb shell wm logging disable-text NEW_FEATURE
+adb shell wm logging enable-text TAG
+adb shell wm logging disable-text TAG
+```
+
+And these commands to enable protologs (in logcat) for WM Shell ([list of all shell tags](/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java)):
+```shell
+adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG
+adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG
 ```
 
 ## Winscope Tracing
 
 The Winscope tool is extremely useful in determining what is happening on-screen in both
 WindowManager and SurfaceFlinger.  Follow [go/winscope](http://go/winscope-help) to learn how to
-use the tool.
+use the tool.  This trace will contain all the information about the windows/activities/surfaces on
+screen.
 
-In addition, there is limited preliminary support for Winscope tracing componetns in the Shell,
-which involves adding trace fields to [wm_shell_trace.proto](frameworks/base/libs/WindowManager/Shell/proto/wm_shell_trace.proto)
-file and ensure it is updated as a part of `WMShell#writeToProto`.
+## WindowManager/SurfaceFlinger hierarchy dump
 
-Tracing can be started via the shell command (to be added to the Winscope tool as needed):
+A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps:
 ```shell
-adb shell cmd statusbar tracing start
-adb shell cmd statusbar tracing stop
+adb shell dumpsys activity containers
+```
+
+Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running:
+```shell
+adb shell dumpsys SurfaceFlinger
+# Search output for "Layer Hierarchy"
+```
+
+## Tracing global SurfaceControl transaction updates
+
+While Winscope traces are very useful, it sometimes doesn't give you enough information about which
+part of the code is initiating the transaction updates.  In such cases, it can be helpful to get
+stack traces when specific surface transaction calls are made, which is possible by enabling the
+following system properties for example:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha  # matches the name of the SurfaceControlTransaction method
+adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface
+adb reboot
+adb logcat -s "SurfaceControlRegistry"
+
+# Disabling logging
+adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\"
+adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\"
+adb reboot
+```
+
+It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
+noisy if unfiltered.
+
+## Tracing activity starts in the app process
+
+It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
+(ie. if you are repro'ing a bug related to activity starts). You can enable this system property to
+get this trace:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.start_activity true
+adb reboot
+adb logcat -s "Instrumentation"
+
+# Disabling
+adb shell setprop persist.wm.debug.start_activity \"\"
+adb reboot
 ```
 
 ## Dumps
@@ -69,6 +117,21 @@
 - Update `WMShell` if you are dumping SysUI state
 - Inject `ShellCommandHandler` into your Shell class, and add a dump callback
 
+## Shell commands
+
+It can be useful to add additional shell commands to drive and test specific interactions.
+
+To add a new command for your feature, inject a `ShellCommandHandler` into your class and add a
+shell command handler in your controller.
+
+```shell
+# List all available commands
+adb shell dumpsys activity service SystemUIService WMShell help
+
+# Run a specific command
+adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...
+```
+
 ## Debugging in Android Studio
 
 If you are using the [go/sysui-studio](http://go/sysui-studio) project, then you can debug Shell
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
index a88ef6a..b489fe8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
@@ -19,25 +19,24 @@
 
 ## Where does the code live
 
-The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](frameworks/base/libs/WindowManager/Shell)
+The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](/libs/WindowManager/Shell)
 directory and is included as a part dependency of the host SystemUI apk.
 
 ## How do I build the Shell library
 
-The library can be built directly by running (using [go/makepush](http://go/makepush)):
+The library can be built directly by running:
 ```shell
-mp :WindowManager-Shell
+m WindowManager-Shell
 ```
 But this is mainly useful for inspecting the contents of the library or verifying it builds. The
-various targets can be found in the Shell library's [Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp)
+various targets can be found in the Shell library's [Android.bp](/libs/WindowManager/Shell/Android.bp)
 file.
 
 Normally, you would build it as a part of the host SystemUI, for example via commandline:
 ```shell
 # Phone SystemUI variant
-mp sysuig
-# Building Shell & SysUI changes along w/ framework changes
-mp core services sysuig
+m SystemUI
+adevice update
 ```
 
 Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md
new file mode 100644
index 0000000..0e20a8e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md
@@ -0,0 +1,30 @@
+# Pattern (one line description)
+
+### What this pattern solves
+
+Give detailed information about the problem that this pattern addresses, include when it
+should/should not be used.
+
+### How it works
+
+Give a high level overview of how this pattern works technically with sufficient links for the
+relevant dependencies for folks to read more.
+
+### Code examples
+
+Explain how this pattern is used in code.
+
+File.kt:
+```kotlin
+fun someFunction() {
+    // Use this code
+}
+```
+
+### Relevant links
+
+Add relevant links to other files that implement this pattern, design docs, or other important
+info.
+
+Link 1: [More info](some_example_link) \
+Link 2: [More info](some_example_link)
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
index d6302e6..30ff669 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
@@ -80,4 +80,6 @@
 # Run a specific command
 adb shell dumpsys activity service SystemUIService WMShell help
 adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...
-```
\ No newline at end of file
+```
+
+More detail can be found in [Debugging in the Shell](debugging.md) section.
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
index 8a80333..98af930 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
@@ -5,7 +5,7 @@
 ## Unit tests
 
 New WM Shell unit tests can be added to the
-[Shell/tests/unittest](frameworks/base/libs/WindowManager/Shell/tests/unittest) directory, and can
+[Shell/tests/unittest](/libs/WindowManager/Shell/tests/unittest) directory, and can
 be run via command line using `atest`:
 ```shell
 atest WMShellUnitTests
@@ -25,10 +25,24 @@
 and SurfaceFlinger traces captured during the run.
 
 New WM Shell Flicker tests can be added to the
-[Shell/tests/flicker](frameworks/base/libs/WindowManager/Shell/tests/flicker) directory, and can
-be run via command line using `atest`:
+[Shell/tests/flicker](/libs/WindowManager/Shell/tests/flicker) directory, and can be run via command line using `atest`:
 ```shell
-atest WMShellFlickerTests
+# Bubbles
+atest WMShellFlickerTestsBubbles
+
+# PIP
+atest WMShellFlickerTestsPip1
+atest WMShellFlickerTestsPip2
+atest WMShellFlickerTestsPip3
+atest WMShellFlickerTestsPipApps
+atest WMShellFlickerTestsPipAppsCSuite
+
+# Splitscreen
+atest WMShellFlickerTestsSplitScreenGroup1
+atest WMShellFlickerTestsSplitScreenGroup2
+
+# Other
+atest WMShellFlickerTestsOther
 ```
 
 **Note**: Currently Flicker tests can only be run from the commandline and not via SysUI Studio
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
index eac74889..9d01535 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
@@ -43,7 +43,7 @@
 
 ## Dagger setup
 
-The threading-related components are provided by the [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java),
+The threading-related components are provided by the [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java),
 for example, the Executors and Handlers for the various threads that are used.  You can request
 an executor of the necessary type by using the appropriate annotation for each of the threads (ie.
 `@ShellMainThread Executor`) when injecting into your Shell component.
@@ -76,7 +76,7 @@
   want to dedupe multiple messages
   - In such cases inject `@ShellMainThread Handler` or use view.getHandler() which should be OK
     assuming that the view root was initialized on the main Shell thread
-- **Never use Looper.getMainLooper()**
+- <u>**Never</u> use Looper.getMainLooper()**
   - It's likely going to be wrong, you can inject `@Main ShellExecutor` to get the SysUI main thread
 
 ### Testing
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/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index e739266..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
@@ -71,11 +71,11 @@
 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/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 238e6b5..c5a0102 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -797,14 +797,20 @@
                     mPipBoundsAlgorithm.getMovementBounds(postChangeBounds),
                     mPipBoundsState.getStashedState());
 
-            updateDisplayLayout.run();
+            // Scale PiP on density dpi change, so it appears to be the same size physically.
+            final boolean densityDpiChanged =
+                    mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
+                    && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
+                            != layout.densityDpi());
+            if (densityDpiChanged) {
+                final float scale = (float) layout.densityDpi()
+                        / mPipDisplayLayoutState.getDisplayLayout().densityDpi();
+                postChangeBounds.set(0, 0,
+                        (int) (postChangeBounds.width() * scale),
+                        (int) (postChangeBounds.height() * scale));
+            }
 
-            // Resize the PiP bounds to be at the same scale relative to the new size spec. For
-            // example, if PiP was resized to 90% of the maximum size on the previous layout,
-            // make sure it is 90% of the new maximum size spec.
-            postChangeBounds.set(0, 0,
-                    (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()),
-                    (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale()));
+            updateDisplayLayout.run();
 
             // Calculate the PiP bounds in the new orientation based on same fraction along the
             // rotated movement bounds.
@@ -821,10 +827,6 @@
             mPipBoundsState.setHasUserResizedPip(true);
             mTouchHandler.setUserResizeBounds(postChangeBounds);
 
-            final boolean densityDpiChanged =
-                    mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
-                            && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
-                            != layout.densityDpi());
             if (densityDpiChanged) {
                 // Using PipMotionHelper#movePip directly here may cause race condition since
                 // the app content in PiP mode may or may not be updated for the new density dpi.
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 d16a692..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
@@ -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/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/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 75965d6..1668e37 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
@@ -143,7 +143,7 @@
                 bubbleController,
                 mainExecutor
             )
-        bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
+        bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData)
     }
 
     @Test
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/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 461dafb..ec1edb8 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -219,6 +219,27 @@
      * 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.
+     *
+     * <p>Prior calls to {@link #addMslAltitudeToLocation(Context, Location)} off the main thread
+     * are necessary to load data from raw assets. Example code on the main thread is as follows:
+     *
+     * <pre>{@code
+     *   if (!mAltitudeConverter.addMslAltitudeToLocation(location)) {
+     *       // Queue up only one call off the main thread.
+     *       if (mIsAltitudeConverterIdle) {
+     *           mIsAltitudeConverterIdle = false;
+     *           executeOffMainThread(() -> {
+     *               try {
+     *                   // Load raw assets for next call attempt on main thread.
+     *                   mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
+     *               } catch (IOException e) {
+     *                   Log.e(TAG, "Not loading raw assets: " + e);
+     *               }
+     *               mIsAltitudeConverterIdle = true;
+     *           });
+     *       }
+     *   }
+     * }</pre>
      */
     @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL)
     public boolean addMslAltitudeToLocation(@NonNull Location location) {
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/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 018eaf6..f110705 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -12,4 +12,11 @@
     namespace: "media_tv"
     description: "Enable the TV client-side AD framework."
     bug: "303506816"
+}
+
+flag {
+    name: "tiaf_v_apis"
+    namespace: "media_tv"
+    description: "TIAF V3.0 APIs for Android V"
+    bug: "303323657"
 }
\ No newline at end of file
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 b97e992..fdda9ea 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -28,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 261154f..c7c2fda 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -25,7 +25,6 @@
             android:id="@android:id/icon1"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text"
             android:layout_centerVertical="true"
             android:layout_alignParentStart="true"
             android:background="@null"/>
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/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index f4641b9..f425f52 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -272,7 +272,7 @@
     <!-- The title of a dialog which asks the user to restore (i.e. re-install, re-download) an app
          after parts of the app have been previously moved into the cloud for temporary storage.
          "installername" is the app that will facilitate the download of the app. [CHAR LIMIT=50] -->
-    <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%1$s</xliff:g>?</string>
+    <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%2$s</xliff:g>?</string>
     <!-- After the user confirms the dialog, a download will start. [CHAR LIMIT=none] -->
     <string name="unarchive_body_text">This app will begin to download in the background</string>
     <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index 9af799c..b20117d 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -25,10 +25,12 @@
 import android.app.Fragment;
 import android.app.FragmentTransaction;
 import android.content.IntentSender;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Process;
+import android.text.TextUtils;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -97,14 +99,30 @@
             String appTitle = pm.getApplicationInfo(mPackageName,
                     PackageManager.ApplicationInfoFlags.of(
                             MATCH_ARCHIVED_PACKAGES)).loadLabel(pm).toString();
-            // TODO(ag/25387215) Get the real installer title here after fixing getInstallSource for
-            //  archived apps.
-            showDialogFragment(appTitle, "installerTitle");
+            String installerTitle = getResponsibleInstallerTitle(pm,
+                    pm.getInstallSourceInfo(mPackageName));
+            showDialogFragment(appTitle, installerTitle);
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Invalid packageName: " + e.getMessage());
         }
     }
 
+    private String getResponsibleInstallerTitle(PackageManager pm,
+            InstallSourceInfo installSource)
+            throws PackageManager.NameNotFoundException {
+        String packageName = TextUtils.isEmpty(installSource.getUpdateOwnerPackageName())
+                ? installSource.getInstallingPackageName()
+                : installSource.getUpdateOwnerPackageName();
+        if (packageName == null) {
+            // Should be unreachable.
+            Log.e(TAG, "Installer not found.");
+            setResult(Activity.RESULT_FIRST_USER);
+            finish();
+            return "";
+        }
+        return pm.getApplicationInfo(packageName, /* flags= */ 0).loadLabel(pm).toString();
+    }
+
     @NonNull
     private String[] getRequestedPermissions(String callingPackage) {
         String[] requestedPermissions = null;
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index a4b3af9..2e64212 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1340,11 +1340,11 @@
     <string name="notice_header" translatable="false"></string>
 
     <!-- Name of the phone device. [CHAR LIMIT=30] -->
-    <string name="media_transfer_this_device_name" product="default">This phone</string>
+    <string name="media_transfer_this_device_name">This phone</string>
     <!-- Name of the tablet device. [CHAR LIMIT=30] -->
-    <string name="media_transfer_this_device_name" product="tablet">This tablet</string>
+    <string name="media_transfer_this_device_name_tablet">This tablet</string>
     <!-- Name of the default media output of the TV. [CHAR LIMIT=30] -->
-    <string name="media_transfer_this_device_name" product="tv">@string/tv_media_transfer_default</string>
+    <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string>
     <!-- Name of the dock device. [CHAR LIMIT=30] -->
     <string name="media_transfer_dock_speaker_device_name">Dock speaker</string>
     <!-- Default name of the external device. [CHAR LIMIT=30] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 15f33d2..ba9180d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -37,6 +37,7 @@
 import android.hardware.hdmi.HdmiPortInfo;
 import android.media.MediaRoute2Info;
 import android.media.RouteListingPreference;
+import android.os.SystemProperties;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -45,6 +46,7 @@
 import com.android.settingslib.R;
 import com.android.settingslib.media.flags.Flags;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -63,6 +65,17 @@
 
     private final DeviceIconUtil mDeviceIconUtil;
 
+    /** Returns this device name for media transfer. */
+    public static @NonNull String getMediaTransferThisDeviceName(@NonNull Context context) {
+        if (isTv(context)) {
+            return context.getString(R.string.media_transfer_this_device_name_tv);
+        } else if (isTablet()) {
+            return context.getString(R.string.media_transfer_this_device_name_tablet);
+        } else {
+            return context.getString(R.string.media_transfer_this_device_name);
+        }
+    }
+
     /** Returns the device name for the given {@code routeInfo}. */
     public static String getSystemRouteNameFromType(
             @NonNull Context context, @NonNull MediaRoute2Info routeInfo) {
@@ -80,7 +93,7 @@
                 name = context.getString(R.string.media_transfer_dock_speaker_device_name);
                 break;
             case TYPE_BUILTIN_SPEAKER:
-                name = context.getString(R.string.media_transfer_this_device_name);
+                name = getMediaTransferThisDeviceName(context);
                 break;
             case TYPE_HDMI:
                 name = context.getString(isTv ? R.string.tv_media_transfer_default :
@@ -135,6 +148,11 @@
                 && Flags.enableTvMediaOutputDialog();
     }
 
+    static boolean isTablet() {
+        return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
+                .contains("tablet");
+    }
+
     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
     @SuppressWarnings("NewApi")
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 3355fb3..6761aa7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -16,29 +16,71 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioDeviceInfo
 import android.media.AudioManager
+import android.media.AudioManager.OnCommunicationDeviceChangedListener
+import androidx.concurrent.futures.DirectExecutor
 import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
-/** Provides audio managing functionality and data. */
+/** Provides audio streams state and managing functionality. */
 interface AudioRepository {
 
     /** Current [AudioManager.getMode]. */
     val mode: StateFlow<Int>
+
+    /**
+     * Ringtone mode.
+     *
+     * @see AudioManager.getRingerModeInternal
+     */
+    val ringerMode: StateFlow<RingerMode>
+
+    /**
+     * Communication device. Emits null when there is no communication device available.
+     *
+     * @see AudioDeviceInfo.getType
+     */
+    val communicationDevice: StateFlow<AudioDeviceInfo?>
+
+    /** State of the [AudioStream]. */
+    suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel>
+
+    /** Current state of the [AudioStream]. */
+    suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel
+
+    suspend fun setVolume(audioStream: AudioStream, volume: Int)
+
+    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean)
 }
 
 class AudioRepositoryImpl(
+    private val context: Context,
     private val audioManager: AudioManager,
-    backgroundCoroutineContext: CoroutineContext,
-    coroutineScope: CoroutineScope,
+    private val backgroundCoroutineContext: CoroutineContext,
+    private val coroutineScope: CoroutineScope,
 ) : AudioRepository {
 
     override val mode: StateFlow<Int> =
@@ -50,4 +92,117 @@
             }
             .flowOn(backgroundCoroutineContext)
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
+
+    private val audioManagerIntents: SharedFlow<String> =
+        callbackFlow {
+                val receiver =
+                    object : BroadcastReceiver() {
+                        override fun onReceive(context: Context?, intent: Intent) {
+                            intent.action?.let { action -> launch { send(action) } }
+                        }
+                    }
+                context.registerReceiver(
+                    receiver,
+                    IntentFilter().apply {
+                        for (action in allActions) {
+                            addAction(action)
+                        }
+                    }
+                )
+
+                awaitClose { context.unregisterReceiver(receiver) }
+            }
+            .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
+
+    override val ringerMode: StateFlow<RingerMode> =
+        audioManagerIntents
+            .filter { ringerActions.contains(it) }
+            .map { RingerMode(audioManager.ringerModeInternal) }
+            .flowOn(backgroundCoroutineContext)
+            .stateIn(
+                coroutineScope,
+                SharingStarted.WhileSubscribed(),
+                RingerMode(audioManager.ringerModeInternal),
+            )
+
+    override val communicationDevice: StateFlow<AudioDeviceInfo?>
+        get() =
+            callbackFlow {
+                    val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
+                    audioManager.addOnCommunicationDeviceChangedListener(
+                        DirectExecutor.INSTANCE,
+                        listener
+                    )
+
+                    awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
+                }
+                .filterNotNull()
+                .map { audioManager.communicationDevice }
+                .flowOn(backgroundCoroutineContext)
+                .stateIn(
+                    coroutineScope,
+                    SharingStarted.WhileSubscribed(),
+                    audioManager.communicationDevice,
+                )
+
+    override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
+        return audioManagerIntents
+            .filter { modelActions.contains(it) }
+            .map { getCurrentAudioStream(audioStream) }
+            .flowOn(backgroundCoroutineContext)
+    }
+
+    override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel {
+        return withContext(backgroundCoroutineContext) {
+            AudioStreamModel(
+                audioStream = audioStream,
+                minVolume = getMinVolume(audioStream),
+                maxVolume = audioManager.getStreamMaxVolume(audioStream.value),
+                volume = audioManager.getStreamVolume(audioStream.value),
+                isAffectedByRingerMode =
+                    audioManager.isStreamAffectedByRingerMode(audioStream.value),
+                isMuted = audioManager.isStreamMute(audioStream.value)
+            )
+        }
+    }
+
+    override suspend fun setVolume(audioStream: AudioStream, volume: Int) =
+        withContext(backgroundCoroutineContext) {
+            audioManager.setStreamVolume(audioStream.value, volume, 0)
+        }
+
+    override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) =
+        withContext(backgroundCoroutineContext) {
+            if (isMuted) {
+                audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_MUTE)
+            } else {
+                audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_UNMUTE)
+            }
+        }
+
+    private fun getMinVolume(stream: AudioStream): Int =
+        try {
+            audioManager.getStreamMinVolume(stream.value)
+        } catch (e: IllegalArgumentException) {
+            // Fallback to STREAM_VOICE_CALL because
+            // CallVolumePreferenceController.java default
+            // return STREAM_VOICE_CALL in getAudioStream
+            audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
+        }
+
+    private companion object {
+        val modelActions =
+            setOf(
+                AudioManager.STREAM_MUTE_CHANGED_ACTION,
+                AudioManager.MASTER_MUTE_CHANGED_ACTION,
+                AudioManager.VOLUME_CHANGED_ACTION,
+                AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+                AudioManager.STREAM_DEVICES_CHANGED_ACTION,
+            )
+        val ringerActions =
+            setOf(
+                AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+            )
+        val allActions = ringerActions + modelActions
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt
new file mode 100644
index 0000000..2b12936
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.settingslib.volume.data.repository
+
+import android.content.Context
+import android.media.AudioSystem
+
+/** Provides the current state of the audio system. */
+interface AudioSystemRepository {
+
+    val isSingleVolume: Boolean
+}
+
+class AudioSystemRepositoryImpl(private val context: Context) : AudioSystemRepository {
+
+    override val isSingleVolume: Boolean
+        get() = AudioSystem.isSingleVolume(context)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
new file mode 100644
index 0000000..58f3c2d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.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.settingslib.volume.shared.model
+
+import android.media.AudioManager
+
+/** Type-safe wrapper for [AudioManager] audio stream. */
+@JvmInline
+value class AudioStream(val value: Int) {
+    init {
+        require(value in supportedStreamTypes) { "Unsupported stream=$value" }
+    }
+
+    private companion object {
+        val supportedStreamTypes =
+            setOf(
+                AudioManager.STREAM_VOICE_CALL,
+                AudioManager.STREAM_SYSTEM,
+                AudioManager.STREAM_RING,
+                AudioManager.STREAM_MUSIC,
+                AudioManager.STREAM_ALARM,
+                AudioManager.STREAM_NOTIFICATION,
+                AudioManager.STREAM_BLUETOOTH_SCO,
+                AudioManager.STREAM_SYSTEM_ENFORCED,
+                AudioManager.STREAM_DTMF,
+                AudioManager.STREAM_TTS,
+                AudioManager.STREAM_ACCESSIBILITY,
+                AudioManager.STREAM_ASSISTANT,
+            )
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt
new file mode 100644
index 0000000..c1be1ee
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.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.settingslib.volume.shared.model
+
+/** Current state of the audio stream. */
+data class AudioStreamModel(
+    val audioStream: AudioStream,
+    val volume: Int,
+    val minVolume: Int,
+    val maxVolume: Int,
+    val isAffectedByRingerMode: Boolean,
+    val isMuted: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt
new file mode 100644
index 0000000..9f03927
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.settingslib.volume.shared.model
+
+import android.media.AudioManager
+
+/** Type-safe wrapper for [AudioManager] ringer mode. */
+@JvmInline
+value class RingerMode(val value: Int) {
+
+    init {
+        require(value in supportedRingerModes) { "Unsupported stream=$value" }
+    }
+
+    private companion object {
+        val supportedRingerModes =
+            setOf(
+                AudioManager.RINGER_MODE_SILENT,
+                AudioManager.RINGER_MODE_VIBRATE,
+                AudioManager.RINGER_MODE_NORMAL,
+                AudioManager.RINGER_MODE_MAX,
+            )
+    }
+}
diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp
index ce3a7ba..f303ab5 100644
--- a/packages/SettingsLib/tests/integ/Android.bp
+++ b/packages/SettingsLib/tests/integ/Android.bp
@@ -40,6 +40,8 @@
         "android.test.runner",
         "telephony-common",
         "android.test.base",
+        "android.test.mock",
+        "truth",
     ],
 
     platform_apis: true,
@@ -49,16 +51,23 @@
         "androidx.test.core",
         "androidx.test.rules",
         "androidx.test.espresso.core",
+        "androidx.test.ext.junit",
         "flag-junit",
-        "mockito-target-minus-junit4",
+        "kotlinx_coroutines_test",
+        "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "truth",
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibSettingsSpinner",
         "SettingsLibUsageProgressBarPreference",
         "settingslib_media_flags_lib",
-        "kotlinx_coroutines_test",
     ],
 
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+        "libstaticjvmtiagent",
+    ],
     dxflags: ["--multi-dex"],
+    manifest: "AndroidManifest.xml",
 }
diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml
index 32048ca..9fb1c1f 100644
--- a/packages/SettingsLib/tests/integ/AndroidManifest.xml
+++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml
@@ -25,7 +25,7 @@
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
-    <application>
+    <application android:debuggable="true" android:testOnly="true">
         <uses-library android:name="android.test.runner" />
         <activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/>
 
diff --git a/packages/SettingsLib/tests/integ/AndroidTest.xml b/packages/SettingsLib/tests/integ/AndroidTest.xml
index d0aee88..9de6019 100644
--- a/packages/SettingsLib/tests/integ/AndroidTest.xml
+++ b/packages/SettingsLib/tests/integ/AndroidTest.xml
@@ -16,6 +16,7 @@
 <configuration description="Runs Tests for SettingsLib.">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
         <option name="test-file-name" value="SettingsLibTests.apk" />
+        <option name="install-arg" value="-t" />
     </target_preparer>
 
     <option name="test-suite-tag" value="apct" />
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
new file mode 100644
index 0000000..7b70c64
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -0,0 +1,281 @@
+/*
+ * 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.settingslib.volume.data.repository
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@Suppress("UnspecifiedRegisterReceiverFlag")
+@RunWith(AndroidJUnit4::class)
+class AudioRepositoryTest {
+
+    @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
+    @Captor
+    private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener>
+    @Captor
+    private lateinit var communicationDeviceListenerCaptor:
+        ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener>
+
+    @Mock private lateinit var context: Context
+    @Mock private lateinit var audioManager: AudioManager
+    @Mock private lateinit var communicationDevice: AudioDeviceInfo
+
+    private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
+    private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf()
+    private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf()
+    private val testScope = TestScope()
+
+    private lateinit var underTest: AudioRepository
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(audioManager.mode).thenReturn(AudioManager.MODE_RINGTONE)
+        `when`(audioManager.communicationDevice).thenReturn(communicationDevice)
+        `when`(audioManager.getStreamMinVolume(anyInt())).thenReturn(MIN_VOLUME)
+        `when`(audioManager.getStreamMaxVolume(anyInt())).thenReturn(MAX_VOLUME)
+        `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+        `when`(audioManager.setStreamVolume(anyInt(), anyInt(), anyInt())).then {
+            volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int
+            triggerIntent(AudioManager.ACTION_VOLUME_CHANGED)
+        }
+        `when`(audioManager.adjustStreamVolume(anyInt(), anyInt(), anyInt())).then {
+            isMuteByStream[it.arguments[0] as Int] = it.arguments[2] == AudioManager.ADJUST_MUTE
+            triggerIntent(AudioManager.STREAM_MUTE_CHANGED_ACTION)
+        }
+        `when`(audioManager.getStreamVolume(anyInt())).thenAnswer {
+            volumeByStream.getOrDefault(it.arguments[0] as Int, 0)
+        }
+        `when`(audioManager.isStreamAffectedByRingerMode(anyInt())).thenAnswer {
+            isAffectedByRingerModeByStream.getOrDefault(it.arguments[0] as Int, false)
+        }
+        `when`(audioManager.isStreamMute(anyInt())).thenAnswer {
+            isMuteByStream.getOrDefault(it.arguments[0] as Int, false)
+        }
+
+        underTest =
+            AudioRepositoryImpl(
+                context,
+                audioManager,
+                testScope.testScheduler,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun audioModeChanges_repositoryEmits() {
+        testScope.runTest {
+            val modes = mutableListOf<Int>()
+            underTest.mode.onEach { modes.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+
+            triggerModeChange(AudioManager.MODE_IN_CALL)
+            runCurrent()
+
+            assertThat(modes).containsExactly(AudioManager.MODE_RINGTONE, AudioManager.MODE_IN_CALL)
+        }
+    }
+
+    @Test
+    fun ringerModeChanges_repositoryEmits() {
+        testScope.runTest {
+            val modes = mutableListOf<RingerMode>()
+            underTest.ringerMode.onEach { modes.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+
+            `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
+            triggerIntent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)
+            runCurrent()
+
+            assertThat(modes)
+                .containsExactly(
+                    RingerMode(AudioManager.RINGER_MODE_NORMAL),
+                    RingerMode(AudioManager.RINGER_MODE_SILENT),
+                )
+        }
+    }
+
+    @Test
+    fun communicationDeviceChanges_repositoryEmits() {
+        testScope.runTest {
+            var device: AudioDeviceInfo? = null
+            underTest.communicationDevice.onEach { device = it }.launchIn(backgroundScope)
+            runCurrent()
+
+            triggerConnectedDeviceChange(communicationDevice)
+            runCurrent()
+
+            assertThat(device).isSameInstanceAs(communicationDevice)
+        }
+    }
+
+    @Test
+    fun adjustingVolume_changesTheStream() {
+        testScope.runTest {
+            val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+            var streamModel: AudioStreamModel? = null
+            underTest
+                .getAudioStream(audioStream)
+                .onEach { streamModel = it }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.setVolume(audioStream, 50)
+            runCurrent()
+
+            assertThat(streamModel)
+                .isEqualTo(
+                    AudioStreamModel(
+                        audioStream = audioStream,
+                        volume = 50,
+                        minVolume = MIN_VOLUME,
+                        maxVolume = MAX_VOLUME,
+                        isAffectedByRingerMode = false,
+                        isMuted = false,
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun adjustingVolume_currentModeIsUpToDate() {
+        testScope.runTest {
+            val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+            var streamModel: AudioStreamModel? = null
+            underTest
+                .getAudioStream(audioStream)
+                .onEach { streamModel = it }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.setVolume(audioStream, 50)
+            runCurrent()
+
+            assertThat(underTest.getCurrentAudioStream(audioStream)).isEqualTo(streamModel)
+        }
+    }
+
+    @Test
+    fun muteStream_mutesTheStream() {
+        testScope.runTest {
+            val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+            var streamModel: AudioStreamModel? = null
+            underTest
+                .getAudioStream(audioStream)
+                .onEach { streamModel = it }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.setMuted(audioStream, true)
+            runCurrent()
+
+            assertThat(streamModel)
+                .isEqualTo(
+                    AudioStreamModel(
+                        audioStream = audioStream,
+                        volume = 0,
+                        minVolume = MIN_VOLUME,
+                        maxVolume = MAX_VOLUME,
+                        isAffectedByRingerMode = false,
+                        isMuted = true,
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun unmuteStream_unmutesTheStream() {
+        testScope.runTest {
+            val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+            isMuteByStream[audioStream.value] = true
+            var streamModel: AudioStreamModel? = null
+            underTest
+                .getAudioStream(audioStream)
+                .onEach { streamModel = it }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.setMuted(audioStream, false)
+            runCurrent()
+
+            assertThat(streamModel)
+                .isEqualTo(
+                    AudioStreamModel(
+                        audioStream = audioStream,
+                        volume = 0,
+                        minVolume = MIN_VOLUME,
+                        maxVolume = MAX_VOLUME,
+                        isAffectedByRingerMode = false,
+                        isMuted = false,
+                    )
+                )
+        }
+    }
+
+    private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
+        verify(audioManager)
+            .addOnCommunicationDeviceChangedListener(
+                any(),
+                communicationDeviceListenerCaptor.capture(),
+            )
+        communicationDeviceListenerCaptor.value.onCommunicationDeviceChanged(communicationDevice)
+    }
+
+    private fun triggerModeChange(mode: Int) {
+        verify(audioManager).addOnModeChangedListener(any(), modeListenerCaptor.capture())
+        modeListenerCaptor.value.onModeChanged(mode)
+    }
+
+    private fun triggerIntent(action: String) {
+        verify(context).registerReceiver(receiverCaptor.capture(), any())
+        receiverCaptor.value.onReceive(context, Intent(action))
+    }
+
+    private companion object {
+        const val MIN_VOLUME = 0
+        const val MAX_VOLUME = 100
+    }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
index 686362f..dddf8e82 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
@@ -16,9 +16,15 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.media.AudioDeviceInfo
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
 
 class FakeAudioRepository : AudioRepository {
 
@@ -26,7 +32,59 @@
     override val mode: StateFlow<Int>
         get() = mutableMode.asStateFlow()
 
+    private val mutableRingerMode = MutableStateFlow(RingerMode(0))
+    override val ringerMode: StateFlow<RingerMode>
+        get() = mutableRingerMode.asStateFlow()
+
+    private val mutableCommunicationDevice = MutableStateFlow<AudioDeviceInfo?>(null)
+    override val communicationDevice: StateFlow<AudioDeviceInfo?>
+        get() = mutableCommunicationDevice.asStateFlow()
+
+    private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf()
+
+    private fun getAudioStreamModelState(
+        audioStream: AudioStream
+    ): MutableStateFlow<AudioStreamModel> =
+        models.getOrPut(audioStream) {
+            MutableStateFlow(
+                AudioStreamModel(
+                    audioStream = audioStream,
+                    volume = 0,
+                    minVolume = 0,
+                    maxVolume = 0,
+                    isAffectedByRingerMode = false,
+                    isMuted = false,
+                )
+            )
+        }
+
+    override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> =
+        getAudioStreamModelState(audioStream).asStateFlow()
+
+    override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel =
+        getAudioStreamModelState(audioStream).value
+
+    override suspend fun setVolume(audioStream: AudioStream, volume: Int) {
+        getAudioStreamModelState(audioStream).update { it.copy(volume = volume) }
+    }
+
+    override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) {
+        getAudioStreamModelState(audioStream).update { it.copy(isMuted = isMuted) }
+    }
+
     fun setMode(newMode: Int) {
         mutableMode.value = newMode
     }
+
+    fun setRingerMode(newRingerMode: RingerMode) {
+        mutableRingerMode.value = newRingerMode
+    }
+
+    fun setCommunicationDevice(device: AudioDeviceInfo?) {
+        mutableCommunicationDevice.value = device
+    }
+
+    fun setAudioStreamModel(model: AudioStreamModel) {
+        getAudioStreamModelState(model.audioStream).update { model }
+    }
 }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
index 3bc1edc..4dbf865 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
@@ -17,8 +17,9 @@
 package com.android.settingslib.volume.domain.interactor
 
 import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
+import com.android.settingslib.BaseTest
 import com.android.settingslib.volume.data.repository.FakeAudioRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -33,7 +34,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class AudioModeInteractorTest {
+class AudioModeInteractorTest : BaseTest() {
 
     private val testScope = TestScope()
     private val fakeAudioRepository = FakeAudioRepository()
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index ceba9be..e2d58d6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -24,6 +24,7 @@
 import static com.android.settingslib.media.PhoneMediaDevice.PHONE_ID;
 import static com.android.settingslib.media.PhoneMediaDevice.USB_HEADSET_ID;
 import static com.android.settingslib.media.PhoneMediaDevice.WIRED_HEADSET_ID;
+import static com.android.settingslib.media.PhoneMediaDevice.getMediaTransferThisDeviceName;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -114,7 +115,7 @@
         when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
 
         assertThat(mPhoneMediaDevice.getName())
-                .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name));
+                .isEqualTo(getMediaTransferThisDeviceName(mContext));
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp
index 6d6e2ff..e2eda4f 100644
--- a/packages/SettingsLib/tests/unit/Android.bp
+++ b/packages/SettingsLib/tests/unit/Android.bp
@@ -32,5 +32,7 @@
         "androidx.test.ext.junit",
         "androidx.test.runner",
         "truth",
+        "kotlinx_coroutines_test",
+        "mockito-target-minus-junit4",
     ],
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 95e0e1b..32c7433 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -908,6 +908,9 @@
     <!-- Permissions required for CTS test - CtsPermissionUiTestCases -->
     <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" />
 
+    <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
+    <uses-permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0ea2b1f..3db99f28 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -370,3 +370,17 @@
    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/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 576596f..4e72dfe 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -160,14 +160,23 @@
                         gridCoordinates
                     ) {
                         detectLongPressGesture { offset ->
-                            isButtonToEditWidgetsShowing = true
-
                             // Deduct both grid offset relative to its container and content offset.
                             val adjustedOffset =
                                 gridCoordinates?.let {
                                     offset - it.positionInWindow() - contentOffset
                                 }
                             val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
+                            // Display the button only when the gesture initiates from widgets,
+                            // the CTA tile, or an empty area on the screen. UMO/smartspace have
+                            // their own long-press handlers. To prevent user confusion, we should
+                            // not display this button.
+                            if (
+                                index == null ||
+                                    communalContent[index].isWidget() ||
+                                    communalContent[index] is CommunalContentModel.CtaTileInViewMode
+                            ) {
+                                isButtonToEditWidgetsShowing = true
+                            }
                             val key = index?.let { keyAtIndexIfEditable(communalContent, index) }
                             viewModel.setSelectedKey(key)
                         }
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 7057545..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
@@ -67,7 +68,7 @@
                     // 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
@@ -82,12 +83,18 @@
                     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 c6851954..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
@@ -55,6 +55,7 @@
 
     // Implementation of [UserActionResult].
     override val toScene: SceneKey = this
+    override val transitionKey: TransitionKey? = null
     override val distance: UserActionDistance? = null
 
     override fun toString(): String {
@@ -104,3 +105,13 @@
         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/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 35754d6..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
@@ -237,7 +237,11 @@
                 }
         swipeTransition.dragOffset += acceleratedOffset
 
-        if (isNewFromScene || result.toScene != swipeTransition.toScene) {
+        if (
+            isNewFromScene ||
+                result.toScene != swipeTransition.toScene ||
+                result.transitionKey != swipeTransition.key
+        ) {
             updateTransition(
                 SwipeTransition(fromScene, result).apply {
                     this.dragOffset = swipeTransition.dragOffset
@@ -484,6 +488,7 @@
 
     private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
         return SwipeTransition(
+            result.transitionKey,
             fromScene,
             layoutImpl.scene(result.toScene),
             computeAbsoluteDistance(fromScene, result),
@@ -491,6 +496,7 @@
     }
 
     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..d904c8b 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 {
@@ -490,6 +504,7 @@
 
         layoutImpl.density = density
         layoutImpl.swipeSourceDetector = swipeSourceDetector
+        layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
     }
 
     layoutImpl.Content(modifier)
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/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index e8cc0ec..2dc94a4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -38,6 +38,8 @@
 import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -47,7 +49,7 @@
 @RunWith(AndroidJUnit4::class)
 class SceneGestureHandlerTest {
     private class TestGestureScope(
-        val coroutineScope: MonotonicClockTestScope,
+        private val testScope: MonotonicClockTestScope,
     ) {
         private val layoutState =
             MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
@@ -92,17 +94,16 @@
                     swipeSourceDetector = DefaultEdgeDetector,
                     transitionInterceptionThreshold = transitionInterceptionThreshold,
                     builder = scenesBuilder,
-                    coroutineScope = coroutineScope,
+                    coroutineScope = testScope,
                 )
                 .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) }
 
         val sceneGestureHandler = layoutImpl.gestureHandler(Orientation.Vertical)
         val horizontalSceneGestureHandler = layoutImpl.gestureHandler(Orientation.Horizontal)
-        val draggable = sceneGestureHandler.draggable
 
         fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) =
             SceneNestedScrollHandler(
-                    layoutImpl,
+                    layoutImpl = layoutImpl,
                     orientation = sceneGestureHandler.orientation,
                     topOrLeftBehavior = nestedScrollBehavior,
                     bottomOrRightBehavior = nestedScrollBehavior,
@@ -117,8 +118,12 @@
         fun up(fractionOfScreen: Float) =
             if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen)
 
-        // Offset y: 10% of the screen
-        val offsetY10 = Offset(x = 0f, y = down(0.1f))
+        fun downOffset(fractionOfScreen: Float) =
+            if (fractionOfScreen < 0f) {
+                error("upOffset() is required, not implemented yet")
+            } else {
+                Offset(x = 0f, y = down(fractionOfScreen))
+            }
 
         val transitionState: TransitionState
             get() = layoutState.transitionState
@@ -127,15 +132,15 @@
             get() = (transitionState as Transition).progress
 
         fun advanceUntilIdle() {
-            coroutineScope.testScheduler.advanceUntilIdle()
+            testScope.testScheduler.advanceUntilIdle()
         }
 
         fun runCurrent() {
-            coroutineScope.testScheduler.runCurrent()
+            testScope.testScheduler.runCurrent()
         }
 
         fun assertIdle(currentScene: SceneKey) {
-            assertWithMessage("transitionState must be Idle").that(transitionState is Idle).isTrue()
+            assertThat(transitionState).isInstanceOf(Idle::class.java)
             assertWithMessage("currentScene does not match")
                 .that(transitionState.currentScene)
                 .isEqualTo(currentScene)
@@ -148,11 +153,8 @@
             progress: Float? = null,
             isUserInputOngoing: Boolean? = null
         ) {
-            val transition = transitionState
-            assertWithMessage("transitionState must be Transition")
-                .that(transition is Transition)
-                .isTrue()
-            transition as Transition
+            assertThat(transitionState).isInstanceOf(Transition::class.java)
+            val transition = transitionState as Transition
 
             if (currentScene != null)
                 assertWithMessage("currentScene does not match")
@@ -180,47 +182,115 @@
                     .that(transition.isUserInputOngoing)
                     .isEqualTo(isUserInputOngoing)
         }
+
+        fun onDragStarted(
+            startedPosition: Offset = Offset.Zero,
+            overSlop: Float,
+            pointersDown: Int = 1
+        ) {
+            // overSlop should be 0f only if the drag gesture starts with startDragImmediately
+            if (overSlop == 0f) error("Consider using onDragStartedImmediately()")
+            onDragStarted(sceneGestureHandler.draggable, startedPosition, overSlop, pointersDown)
+        }
+
+        fun onDragStartedImmediately(startedPosition: Offset = Offset.Zero, pointersDown: Int = 1) {
+            onDragStarted(
+                sceneGestureHandler.draggable,
+                startedPosition,
+                overSlop = 0f,
+                pointersDown
+            )
+        }
+
+        fun onDragStarted(
+            draggableHandler: DraggableHandler,
+            startedPosition: Offset = Offset.Zero,
+            overSlop: Float = 0f,
+            pointersDown: Int = 1
+        ) {
+            draggableHandler.onDragStarted(
+                startedPosition = startedPosition,
+                overSlop = overSlop,
+                pointersDown = pointersDown,
+            )
+
+            // MultiPointerDraggable will always call onDelta with the initial overSlop right after
+            onDelta(pixels = overSlop)
+        }
+
+        fun onDelta(pixels: Float) {
+            sceneGestureHandler.draggable.onDelta(pixels = pixels)
+        }
+
+        fun onDragStopped(velocity: Float) {
+            sceneGestureHandler.draggable.onDragStopped(velocity = velocity)
+            runCurrent()
+        }
+
+        fun NestedScrollConnection.scroll(
+            available: Offset,
+            consumedByScroll: Offset = Offset.Zero,
+        ) {
+            val consumedByPreScroll =
+                onPreScroll(
+                    available = available,
+                    source = NestedScrollSource.Drag,
+                )
+            val consumed = consumedByPreScroll + consumedByScroll
+
+            onPostScroll(
+                consumed = consumed,
+                available = available - consumed,
+                source = NestedScrollSource.Drag
+            )
+        }
+
+        fun NestedScrollConnection.preFling(
+            available: Velocity,
+            coroutineScope: CoroutineScope = testScope,
+        ) {
+            // onPreFling is a suspend function that returns the consumed velocity once it finishes
+            // consuming it. In the current scenario, it returns after completing the animation.
+            // To return immediately, we can initiate a job that allows us to check the status
+            // before the animation starts.
+            coroutineScope.launch { onPreFling(available = available) }
+            runCurrent()
+        }
     }
 
     private fun runGestureTest(block: suspend TestGestureScope.() -> Unit) {
-        runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
-    }
+        runMonotonicClockTest {
+            val testGestureScope = TestGestureScope(testScope = this)
 
-    private fun DraggableHandler.onDragStarted(
-        overSlop: Float = 0f,
-        startedPosition: Offset = Offset.Zero,
-    ) {
-        onDragStarted(startedPosition, overSlop)
-        // MultiPointerDraggable will always call onDelta with the initial overSlop right after
-        onDelta(overSlop)
+            // run the test
+            testGestureScope.block()
+        }
     }
 
     @Test fun testPreconditions() = runGestureTest { assertIdle(currentScene = SceneA) }
 
     @Test
     fun onDragStarted_shouldStartATransition() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
         assertThat(progress).isEqualTo(0.1f)
 
-        draggable.onDelta(pixels = down(0.1f))
+        onDelta(pixels = down(fractionOfScreen = 0.1f))
         assertThat(progress).isEqualTo(0.2f)
     }
 
     @Test
     fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
-        draggable.onDragStopped(
-            velocity = velocityThreshold - 0.01f,
-        )
+        onDragStopped(velocity = velocityThreshold - 0.01f)
         assertTransition(currentScene = SceneA)
 
         // wait for the stop animation
@@ -230,11 +300,10 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
-        draggable.onDragStopped(velocity = velocityThreshold)
-
+        onDragStopped(velocity = velocityThreshold)
         assertTransition(currentScene = SceneC)
 
         // wait for the stop animation
@@ -244,10 +313,10 @@
 
     @Test
     fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
-        draggable.onDragStopped(velocity = 0f)
+        onDragStopped(velocity = 0f)
         advanceUntilIdle()
         assertIdle(currentScene = SceneA)
     }
@@ -255,7 +324,7 @@
     @Test
     fun onDragReversedDirection_changeToScene() = runGestureTest {
         // Drag A -> B with progress 0.6
-        draggable.onDragStarted(overSlop = -60f)
+        onDragStarted(overSlop = -60f)
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -264,7 +333,7 @@
         )
 
         // Reverse direction such that A -> C now with 0.4
-        draggable.onDelta(100f)
+        onDelta(pixels = 100f)
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -273,7 +342,7 @@
         )
 
         // After the drag stopped scene C should be committed
-        draggable.onDragStopped(velocity = velocityThreshold)
+        onDragStopped(velocity = velocityThreshold)
         assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC)
 
         // wait for the stop animation
@@ -283,9 +352,12 @@
 
     @Test
     fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest {
-        horizontalSceneGestureHandler.draggable.onDragStarted(up(0.3f))
+        val horizontalDraggableHandler = horizontalSceneGestureHandler.draggable
+
+        onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f))
         assertIdle(currentScene = SceneA)
-        horizontalSceneGestureHandler.draggable.onDragStarted(down(0.3f))
+
+        onDragStarted(horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f))
         assertIdle(currentScene = SceneA)
     }
 
@@ -294,7 +366,7 @@
         navigateToSceneC()
 
         // We are on SceneC which has no action in Down direction
-        draggable.onDragStarted(10f)
+        onDragStarted(overSlop = 10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -303,7 +375,7 @@
         )
 
         // Reverse drag direction, it will consume the previous drag
-        draggable.onDelta(-10f)
+        onDelta(pixels = -10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -312,7 +384,7 @@
         )
 
         // Continue reverse drag direction, it should record progress to Scene B
-        draggable.onDelta(-10f)
+        onDelta(pixels = -10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -326,7 +398,10 @@
         navigateToSceneC()
 
         // Start dragging from the bottom
-        draggable.onDragStarted(up(0.1f), Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE))
+        onDragStarted(
+            startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE),
+            overSlop = up(fractionOfScreen = 0.1f)
+        )
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -337,14 +412,14 @@
 
     @Test
     fun onDragToExactlyZero_toSceneIsSet() = runGestureTest {
-        draggable.onDragStarted(down(0.3f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.3f))
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
             toScene = SceneC,
             progress = 0.3f
         )
-        draggable.onDelta(up(0.3f))
+        onDelta(pixels = up(fractionOfScreen = 0.3f))
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -355,8 +430,8 @@
 
     private fun TestGestureScope.navigateToSceneC() {
         assertIdle(currentScene = SceneA)
-        draggable.onDragStarted(down(1f))
-        draggable.onDragStopped(0f)
+        onDragStarted(overSlop = down(fractionOfScreen = 1f))
+        onDragStopped(velocity = 0f)
         advanceUntilIdle()
         assertIdle(currentScene = SceneC)
     }
@@ -364,7 +439,7 @@
     @Test
     fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest {
         // Drag A -> B with progress 0.2
-        draggable.onDragStarted(overSlop = up(0.2f))
+        onDragStarted(overSlop = up(fractionOfScreen = 0.2f))
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -373,13 +448,13 @@
         )
 
         // Start animation A -> B with progress 0.2 -> 1.0
-        draggable.onDragStopped(velocity = -velocityThreshold)
+        onDragStopped(velocity = -velocityThreshold)
         assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
 
         // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change
         // the transition to B -> C with progress 0.2
-        draggable.onDragStarted()
-        draggable.onDelta(up(1f))
+        onDragStartedImmediately()
+        onDelta(pixels = up(fractionOfScreen = 1f))
         assertTransition(
             currentScene = SceneB,
             fromScene = SceneB,
@@ -388,7 +463,7 @@
         )
 
         // After the drag stopped scene C should be committed
-        draggable.onDragStopped(velocity = -velocityThreshold)
+        onDragStopped(velocity = -velocityThreshold)
         assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC)
 
         // wait for the stop animation
@@ -398,9 +473,9 @@
 
     @Test
     fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest {
-        draggable.onDragStarted(overSlop = up(0.2f))
-        draggable.onDelta(up(0.2f))
-        draggable.onDragStopped(velocity = -velocityThreshold)
+        onDragStarted(overSlop = up(fractionOfScreen = 0.2f))
+        onDelta(pixels = up(fractionOfScreen = 0.2f))
+        onDragStopped(velocity = -velocityThreshold)
         assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
 
         mutableUserActionsA.remove(Swipe.Up)
@@ -409,83 +484,79 @@
         mutableUserActionsB.remove(Swipe.Down)
 
         // start accelaratedScroll and scroll over to B -> null
-        draggable.onDragStarted()
-        draggable.onDelta(up(0.5f))
-        draggable.onDelta(up(0.5f))
+        onDragStartedImmediately()
+        onDelta(pixels = up(fractionOfScreen = 0.5f))
+        onDelta(pixels = up(fractionOfScreen = 0.5f))
 
         // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may
         // still be called. Make sure that they don't crash or change the scene
-        draggable.onDelta(up(0.5f))
-        draggable.onDragStopped(0f)
+        onDelta(pixels = up(fractionOfScreen = 0.5f))
+        onDragStopped(velocity = 0f)
 
         advanceUntilIdle()
         assertIdle(SceneB)
 
         // These events can still come in after the animation has settled
-        draggable.onDelta(up(0.5f))
-        draggable.onDragStopped(0f)
+        onDelta(pixels = up(fractionOfScreen = 0.5f))
+        onDragStopped(velocity = 0f)
         assertIdle(SceneB)
     }
 
     @Test
     fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest {
-        draggable.onDragStarted(up(0.1f))
+        onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
 
         mutableUserActionsA[Swipe.Up] = SceneC
-        draggable.onDelta(up(0.1f))
+        onDelta(pixels = up(fractionOfScreen = 0.1f))
         // target stays B even though UserActions changed
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f)
-        draggable.onDragStopped(down(0.1f))
+        onDragStopped(velocity = down(fractionOfScreen = 0.1f))
         advanceUntilIdle()
 
         // now target changed to C for new drag
-        draggable.onDragStarted(up(0.1f))
+        onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.1f)
     }
 
     @Test
     fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest {
-        draggable.onDragStarted(up(0.1f))
+        onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
 
         mutableUserActionsA[Swipe.Up] = SceneC
-        draggable.onDelta(up(0.1f))
-        draggable.onDragStopped(down(0.1f))
+        onDelta(pixels = up(fractionOfScreen = 0.1f))
+        onDragStopped(velocity = down(fractionOfScreen = 0.1f))
 
         // now target changed to C for new drag that started before previous drag settled to Idle
-        draggable.onDragStarted(overSlop = 0f)
-        draggable.onDelta(up(0.1f))
+        onDragStartedImmediately()
+        onDelta(pixels = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f)
     }
 
     @Test
     fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
-        draggable.onDragStarted(overSlop = down(0.1f))
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
-        draggable.onDragStopped(
-            velocity = velocityThreshold,
-        )
+        onDragStopped(velocity = velocityThreshold)
 
-        // The stop animation is not started yet
-        assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
-
-        runCurrent()
-
-        assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
-        assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
         assertTransition(currentScene = SceneC)
+        assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
+        assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
 
         // Start a new gesture while the offset is animating
-        draggable.onDragStarted()
+        onDragStartedImmediately()
         assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
     }
 
     @Test
     fun onInitialPreScroll_EdgeWithOverscroll_doNotChangeState() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
-        nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
+        nestedScroll.onPreScroll(
+            available = downOffset(fractionOfScreen = 0.1f),
+            source = NestedScrollSource.Drag
+        )
         assertIdle(currentScene = SceneA)
     }
 
@@ -509,40 +580,29 @@
         val consumed =
             nestedScroll.onPostScroll(
                 consumed = Offset.Zero,
-                available = offsetY10,
+                available = downOffset(fractionOfScreen = 0.1f),
                 source = NestedScrollSource.Drag
             )
 
         assertTransition(currentScene = SceneA)
         assertThat(progress).isEqualTo(0.1f)
-        assertThat(consumed).isEqualTo(offsetY10)
-    }
-
-    private fun NestedScrollConnection.scroll(
-        available: Offset,
-        consumedByScroll: Offset = Offset.Zero,
-    ) {
-        val consumedByPreScroll =
-            onPreScroll(available = available, source = NestedScrollSource.Drag)
-        val consumed = consumedByPreScroll + consumedByScroll
-        onPostScroll(
-            consumed = consumed,
-            available = available - consumed,
-            source = NestedScrollSource.Drag
-        )
+        assertThat(consumed).isEqualTo(downOffset(fractionOfScreen = 0.1f))
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptPreScrollEvents() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
         assertThat(progress).isEqualTo(0.1f)
 
         // start intercept preScroll
         val consumed =
-            nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
+            nestedScroll.onPreScroll(
+                available = downOffset(fractionOfScreen = 0.1f),
+                source = NestedScrollSource.Drag
+            )
         assertThat(progress).isEqualTo(0.2f)
 
         // do nothing on postScroll
@@ -553,12 +613,12 @@
         )
         assertThat(progress).isEqualTo(0.2f)
 
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
         assertThat(progress).isEqualTo(0.3f)
         assertTransition(currentScene = SceneA)
     }
 
-    private suspend fun TestGestureScope.preScrollAfterSceneTransition(
+    private fun TestGestureScope.preScrollAfterSceneTransition(
         firstScroll: Float,
         secondScroll: Float
     ) {
@@ -567,10 +627,13 @@
         nestedScroll.scroll(available = Offset(0f, firstScroll))
 
         // stop scene transition (start the "stop animation")
-        nestedScroll.onPreFling(available = Velocity.Zero)
+        nestedScroll.preFling(available = Velocity.Zero)
 
         // a pre scroll event, that could be intercepted by SceneGestureHandler
-        nestedScroll.onPreScroll(Offset(0f, secondScroll), NestedScrollSource.Drag)
+        nestedScroll.onPreScroll(
+            available = Offset(0f, secondScroll),
+            source = NestedScrollSource.Drag
+        )
     }
 
     @Test
@@ -610,16 +673,17 @@
 
         preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
 
+        advanceUntilIdle()
         assertIdle(SceneB)
     }
 
     @Test
     fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
 
-        nestedScroll.onPreFling(available = Velocity.Zero)
+        nestedScroll.preFling(available = Velocity.Zero)
         assertTransition(currentScene = SceneA)
 
         // wait for the stop animation
@@ -627,15 +691,15 @@
         assertIdle(currentScene = SceneA)
     }
 
-    private suspend fun TestGestureScope.flingAfterScroll(
+    private fun TestGestureScope.flingAfterScroll(
         use: NestedScrollBehavior,
         idleAfterScroll: Boolean,
     ) {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use)
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
         if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA)
 
-        nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
+        nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
     }
 
     @Test
@@ -679,19 +743,22 @@
     }
 
     /** we started the scroll in the scene, then fling with the velocityThreshold */
-    private suspend fun TestGestureScope.flingAfterScrollStartedInScene(
+    private fun TestGestureScope.flingAfterScrollStartedInScene(
         use: NestedScrollBehavior,
         idleAfterScroll: Boolean,
     ) {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use)
         // scroll consumed in child
-        nestedScroll.scroll(available = offsetY10, consumedByScroll = offsetY10)
+        nestedScroll.scroll(
+            available = downOffset(fractionOfScreen = 0.1f),
+            consumedByScroll = downOffset(fractionOfScreen = 0.1f)
+        )
 
         // scroll offsetY10 is all available for parents
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
         if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA)
 
-        nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
+        nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
     }
 
     @Test
@@ -732,20 +799,20 @@
 
     @Test
     fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest {
-        draggable.onDelta(down(0.1f))
+        onDelta(pixels = down(fractionOfScreen = 0.1f))
         assertIdle(currentScene = SceneA)
     }
 
     @Test
     fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
-        draggable.onDragStopped(velocityThreshold)
+        onDragStopped(velocity = velocityThreshold)
         assertIdle(currentScene = SceneA)
     }
 
     @Test
     fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
-        nestedScroll.onPreFling(Velocity(0f, velocityThreshold))
+        nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
         assertIdle(currentScene = SceneA)
     }
 
@@ -753,8 +820,10 @@
     fun startNestedScrollWhileDragging() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
 
+        val offsetY10 = downOffset(fractionOfScreen = 0.1f)
+
         // Start a drag and then stop it, given that
-        draggable.onDragStarted(overSlop = up(0.1f))
+        onDragStarted(overSlop = up(0.1f))
 
         assertTransition(currentScene = SceneA)
         assertThat(progress).isEqualTo(0.1f)
@@ -764,7 +833,7 @@
         assertThat(progress).isEqualTo(0.2f)
 
         // this should be ignored, we are scrolling now!
-        draggable.onDragStopped(-velocityThreshold)
+        onDragStopped(-velocityThreshold)
         assertTransition(currentScene = SceneA)
 
         nestedScroll.scroll(available = -offsetY10)
@@ -773,7 +842,7 @@
         nestedScroll.scroll(available = -offsetY10)
         assertThat(progress).isEqualTo(0.4f)
 
-        nestedScroll.onPreFling(available = Velocity(0f, -velocityThreshold))
+        nestedScroll.preFling(available = Velocity(0f, -velocityThreshold))
         assertTransition(currentScene = SceneB)
 
         // wait for the stop animation
@@ -788,7 +857,7 @@
 
         // Swipe up from the middle to transition to scene B.
         val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
-        draggable.onDragStarted(startedPosition = middle, overSlop = up(0.1f))
+        onDragStarted(startedPosition = middle, overSlop = up(0.1f))
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -802,7 +871,7 @@
         // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted()
         // should be 0f.
         assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue()
-        draggable.onDragStarted(startedPosition = middle, overSlop = 0f)
+        onDragStartedImmediately(startedPosition = middle)
 
         // We should have intercepted the transition, so the transition should be the same object.
         assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB)
@@ -813,7 +882,7 @@
         // instead animate from C to A.
         val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE)
         assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse()
-        draggable.onDragStarted(startedPosition = bottom, overSlop = up(0.1f))
+        onDragStarted(startedPosition = bottom, overSlop = up(0.1f))
 
         assertTransition(
             currentScene = SceneC,
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 9a25d43..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()
@@ -116,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 af1a5b8..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())
                 }
@@ -492,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/src/com/android/compose/test/RunMonotonicClockTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
index fbcd5b2..0245cf2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
@@ -3,7 +3,9 @@
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestMonotonicFrameClock
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
 
@@ -12,16 +14,38 @@
  * function.
  *
  * The [TestCoroutineScheduler] is passed to provide the functionality to wait for idle.
+ *
+ * Note: Please refer to the documentation for [runTest], as this feature utilizes it. This will
+ * provide a comprehensive understanding of all its behaviors.
  */
-@OptIn(ExperimentalTestApi::class)
+@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class)
 fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) = runTest {
-    // We need a CoroutineScope (like a TestScope) to create a TestMonotonicFrameClock.
-    withContext(TestMonotonicFrameClock(this)) {
-        MonotonicClockTestScope(coroutineScope = this, testScheduler = testScheduler).block()
+    val testScope: TestScope = this
+
+    withContext(TestMonotonicFrameClock(coroutineScope = testScope)) {
+        val testScopeWithMonotonicFrameClock: CoroutineScope = this
+
+        val scope =
+            MonotonicClockTestScope(
+                testScope = testScopeWithMonotonicFrameClock,
+                testScheduler = testScope.testScheduler,
+                backgroundScope = backgroundScope,
+            )
+
+        // Run the test
+        scope.block()
     }
 }
 
+/**
+ * A coroutine scope that for launching test coroutines for Compose.
+ *
+ * @param testScheduler The delay-skipping scheduler used by the test dispatchers running the code
+ *   in this scope (see [TestScope.testScheduler]).
+ * @param backgroundScope A scope for background work (see [TestScope.backgroundScope]).
+ */
 class MonotonicClockTestScope(
-    coroutineScope: CoroutineScope,
-    val testScheduler: TestCoroutineScheduler
-) : CoroutineScope by coroutineScope
+    testScope: CoroutineScope,
+    val testScheduler: TestCoroutineScheduler,
+    val backgroundScope: CoroutineScope,
+) : CoroutineScope by testScope
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/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index b6605ed..fa47a02 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
@@ -59,15 +59,21 @@
     }
 
     @Test
+    fun isEnabled_settingNotInitialized_returnsFalseByDefault() =
+        scope.runTest {
+            val actualValue by collectLastValue(underTest.isEnabled(testUser1))
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isFalse()
+        }
+
+    @Test
     fun isEnabled_initiallyGetsSettingsValue() =
         scope.runTest {
             val actualValue by collectLastValue(underTest.isEnabled(testUser1))
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                ENABLED,
-                testUser1.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
 
             Truth.assertThat(actualValue).isTrue()
@@ -78,25 +84,13 @@
         scope.runTest {
             val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1))
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                DISABLED,
-                testUser1.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                ENABLED,
-                testUser1.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                DISABLED,
-                testUser1.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
 
             Truth.assertThat(flowValues.size).isEqualTo(3)
@@ -109,26 +103,14 @@
             val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1))
             val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2))
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                DISABLED,
-                testUser1.identifier
-            )
-            settings.putIntForUser(
-                SETTING_NAME,
-                DISABLED,
-                testUser2.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier)
             runCurrent()
 
             Truth.assertThat(lastValueUser1).isFalse()
             Truth.assertThat(lastValueUser2).isFalse()
 
-            settings.putIntForUser(
-                SETTING_NAME,
-                ENABLED,
-                testUser1.identifier
-            )
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
 
             Truth.assertThat(lastValueUser1).isTrue()
@@ -142,11 +124,7 @@
             runCurrent()
             Truth.assertThat(success).isTrue()
 
-            val actualValue =
-                settings.getIntForUser(
-                    SETTING_NAME,
-                    testUser1.identifier
-                )
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             Truth.assertThat(actualValue).isEqualTo(ENABLED)
         }
 
@@ -157,11 +135,7 @@
             runCurrent()
             Truth.assertThat(success).isTrue()
 
-            val actualValue =
-                settings.getIntForUser(
-                    SETTING_NAME,
-                    testUser1.identifier
-                )
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             Truth.assertThat(actualValue).isEqualTo(DISABLED)
         }
 
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 30eb782..3d8159e 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
@@ -24,6 +24,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -59,6 +60,16 @@
     }
 
     @Test
+    fun isEnabled_settingNotInitialized_returnsFalseByDefault() =
+        scope.runTest {
+            val actualValue by collectLastValue(underTest.isEnabled(testUser1))
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isFalse()
+        }
+
+    @Test
     fun isEnabled_initiallyGetsSettingsValue() =
         scope.runTest {
             val actualValue by collectLastValue(underTest.isEnabled(testUser1))
@@ -72,8 +83,7 @@
     @Test
     fun isEnabled_settingUpdated_valueUpdated() =
         scope.runTest {
-            val flowValues: List<Boolean> by
-                collectValues(underTest.isEnabled(testUser1))
+            val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1))
 
             settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             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 76b0d4a..1642e52 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
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
@@ -44,27 +45,32 @@
 class CommunalMediaRepositoryImplTest : SysuiTestCase() {
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var mediaData: MediaData
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
+    private lateinit var underTest: CommunalMediaRepositoryImpl
 
     private val mediaDataListenerCaptor: KotlinArgumentCaptor<MediaDataManager.Listener> by lazy {
         KotlinArgumentCaptor(MediaDataManager.Listener::class.java)
     }
 
-    private lateinit var mediaRepository: CommunalMediaRepository
-
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
+        underTest =
+            CommunalMediaRepositoryImpl(
+                mediaDataManager,
+                tableLogBuffer,
+            )
     }
 
     @Test
     fun hasAnyMediaOrRecommendation_defaultsToFalse() =
         testScope.runTest {
-            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
-
-            val mediaModel = collectLastValue(mediaRepository.mediaModel)
+            val mediaModel = collectLastValue(underTest.mediaModel)
             runCurrent()
             assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
         }
@@ -72,13 +78,11 @@
     @Test
     fun mediaModel_updatesWhenMediaDataLoaded() =
         testScope.runTest {
-            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
-
             // Listener is added
             verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
 
             // Initial value is false.
-            val mediaModel = collectLastValue(mediaRepository.mediaModel)
+            val mediaModel = collectLastValue(underTest.mediaModel)
             runCurrent()
             assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
 
@@ -96,8 +100,6 @@
     @Test
     fun mediaModel_updatesWhenMediaDataRemoved() =
         testScope.runTest {
-            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
-
             // Listener is added
             verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
 
@@ -107,7 +109,7 @@
             runCurrent()
 
             // Media active now returns true.
-            val mediaModel = collectLastValue(mediaRepository.mediaModel)
+            val mediaModel = collectLastValue(underTest.mediaModel)
             assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue()
 
             // Change to media unavailable and notify the listener.
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 d15e15e..6bff0dc7 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
@@ -25,6 +25,8 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
@@ -36,11 +38,15 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @android.platform.test.annotations.EnabledOnRavenwood
 class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
     private lateinit var underTest: CommunalPrefsRepositoryImpl
 
     private val kosmos = testKosmos()
@@ -51,6 +57,8 @@
 
     @Before
     fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
         userRepository = kosmos.fakeUserRepository
         userRepository.setUserInfos(USER_INFOS)
 
@@ -67,6 +75,8 @@
                 kosmos.testDispatcher,
                 userRepository,
                 userFileManager,
+                logcatLogBuffer("CommunalPrefsRepositoryImplTest"),
+                tableLogBuffer,
             )
     }
 
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 0c66bbb..2911a50 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
@@ -26,6 +26,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.settings.FakeSettings
@@ -34,12 +35,15 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @android.platform.test.annotations.EnabledOnRavenwood
 class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
@@ -64,6 +68,7 @@
                 userRepository,
                 secureSettings,
                 logcatLogBuffer("CommunalTutorialRepositoryImplTest"),
+                tableLogBuffer,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index c979ca6..475179d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -198,13 +198,22 @@
         }
 
     @Test
-    fun deleteWidget_removeWidgetId_andDeleteFromDb() =
+    fun deleteWidgetFromDb() =
         testScope.runTest {
             val id = 1
-            underTest.deleteWidget(id)
+            underTest.deleteWidgetFromDb(id)
             runCurrent()
 
             verify(communalWidgetDao).deleteWidgetById(id)
+        }
+
+    @Test
+    fun deleteWidgetFromHost() =
+        testScope.runTest {
+            val id = 1
+            underTest.deleteWidgetFromHost(id)
+            runCurrent()
+
             verify(appWidgetHost).deleteAppWidgetId(id)
         }
 
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/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 273d1cd..cf727cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
@@ -82,6 +83,7 @@
                 kosmos.communalInteractor,
                 mediaHost,
                 uiEventLogger,
+                logcatLogBuffer("CommunalEditModeViewModelTest"),
             )
     }
 
@@ -142,6 +144,49 @@
         }
 
     @Test
+    fun deleteWidget() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+            // Widgets available.
+            val widgets =
+                listOf(
+                    CommunalWidgetContentModel(
+                        appWidgetId = 0,
+                        priority = 30,
+                        providerInfo = mock(),
+                    ),
+                    CommunalWidgetContentModel(
+                        appWidgetId = 1,
+                        priority = 20,
+                        providerInfo = mock(),
+                    ),
+                )
+            widgetRepository.setCommunalWidgets(widgets)
+
+            val communalContent by collectLastValue(underTest.communalContent)
+
+            // Widgets and CTA tile are shown.
+            assertThat(communalContent?.size).isEqualTo(3)
+            assertThat(communalContent?.get(0))
+                .isInstanceOf(CommunalContentModel.Widget::class.java)
+            assertThat(communalContent?.get(1))
+                .isInstanceOf(CommunalContentModel.Widget::class.java)
+            assertThat(communalContent?.get(2))
+                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
+
+            underTest.onDeleteWidget(widgets.get(0).appWidgetId)
+
+            // Only one widget and CTA tile remain.
+            assertThat(communalContent?.size).isEqualTo(2)
+            val item = communalContent?.get(0)
+            val appWidgetId = if (item is CommunalContentModel.Widget) item.appWidgetId else null
+            assertThat(appWidgetId).isEqualTo(widgets.get(1).appWidgetId)
+            assertThat(communalContent?.get(1))
+                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
+        }
+
+    @Test
     fun reorderWidget_uiEventLogging_start() {
         underTest.onReorderWidgetStart()
         verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 0723e83..73d3091 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
@@ -99,6 +100,7 @@
                 kosmos.communalInteractor,
                 kosmos.communalTutorialInteractor,
                 mediaHost,
+                logcatLogBuffer("CommunalViewModelTest"),
             )
     }
 
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/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/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/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/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index fa89fcd..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>
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/systemui/accessibility/data/repository/ColorCorrectionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt
index c7e5b64..c23a051 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt
@@ -61,7 +61,8 @@
                 .observerFlow(userHandle.identifier, SETTING_NAME)
                 .onStart { emit(Unit) }
                 .map {
-                    secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED
+                    secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) ==
+                        ENABLED
                 }
                 .distinctUntilChanged()
                 .flowOn(bgCoroutineContext)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt
index 419eada..8525797 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt
@@ -61,7 +61,8 @@
                 .observerFlow(userHandle.identifier, SETTING_NAME)
                 .onStart { emit(Unit) }
                 .map {
-                    secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED
+                    secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) ==
+                        ENABLED
                 }
                 .distinctUntilChanged()
                 .flowOn(bgCoroutineContext)
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/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 4176083..4ea5f4c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -198,11 +198,13 @@
                                 UdfpsTouchOverlayBinder.bind(
                                     view = this,
                                     viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
+                                    udfpsOverlayInteractor = udfpsOverlayInteractor,
                                 )
                             else ->
                                 UdfpsTouchOverlayBinder.bind(
                                     view = this,
                                     viewModel = defaultUdfpsTouchOverlayViewModel.get(),
+                                    udfpsOverlayInteractor = udfpsOverlayInteractor,
                                 )
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
index bb6a68b..2e29c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.biometrics.ui.binder
 
+import android.util.Log
 import androidx.core.view.isInvisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay
 import com.android.systemui.biometrics.ui.viewmodel.UdfpsTouchOverlayViewModel
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -31,19 +33,26 @@
 
     /**
      * Updates visibility for the UdfpsTouchOverlay which controls whether the view will receive
-     * touches or not.
+     * touches or not. For some devices, this is instead handled by UdfpsOverlayInteractor, so this
+     * viewBinder will send the information to the interactor.
      */
     @JvmStatic
     fun bind(
         view: UdfpsTouchOverlay,
         viewModel: UdfpsTouchOverlayViewModel,
+        udfpsOverlayInteractor: UdfpsOverlayInteractor,
     ) {
         if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) return
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
                     viewModel.shouldHandleTouches.collect { shouldHandleTouches ->
+                        Log.d(
+                            "UdfpsTouchOverlayBinder",
+                            "[$view]: update shouldHandleTouches=$shouldHandleTouches"
+                        )
                         view.isInvisible = !shouldHandleTouches
+                        udfpsOverlayInteractor.setHandleTouches(shouldHandleTouches)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 779446d..3b727d2 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -97,7 +97,7 @@
     fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
 
     @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id")
-    fun getWidgetByIdNow(id: Int): CommunalWidgetItem
+    fun getWidgetByIdNow(id: Int): CommunalWidgetItem?
 
     @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem)
 
@@ -120,7 +120,9 @@
     fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
         widgetIdToPriorityMap.forEach { (id, priority) ->
             val widget = getWidgetByIdNow(id)
-            updateItemRank(widget.itemId, priority)
+            if (widget != null) {
+                updateItemRank(widget.itemId, priority)
+            }
         }
     }
 
@@ -134,9 +136,13 @@
     }
 
     @Transaction
-    fun deleteWidgetById(widgetId: Int) {
-        val widget = getWidgetByIdNow(widgetId)
+    fun deleteWidgetById(widgetId: Int): Boolean {
+        val widget =
+            getWidgetByIdNow(widgetId) ?: // no entry to delete from db
+            return false
+
         deleteItemRankById(widget.itemId)
         deleteWidgets(widget)
+        return true
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
index cf2e33c..c46f0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
@@ -16,15 +16,34 @@
 
 package com.android.systemui.communal.data.model
 
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
 /** Data model of media on the communal hub. */
 data class CommunalMediaModel(
     val hasAnyMediaOrRecommendation: Boolean,
     val createdTimestampMillis: Long = 0L,
-) {
+) : Diffable<CommunalMediaModel> {
     companion object {
         val INACTIVE =
             CommunalMediaModel(
                 hasAnyMediaOrRecommendation = false,
             )
     }
+
+    override fun logDiffs(prevVal: CommunalMediaModel, row: TableRowLogger) {
+        if (hasAnyMediaOrRecommendation != prevVal.hasAnyMediaOrRecommendation) {
+            row.logChange(
+                columnName = "isMediaActive",
+                value = hasAnyMediaOrRecommendation,
+            )
+        }
+
+        if (createdTimestampMillis != prevVal.createdTimestampMillis) {
+            row.logChange(
+                columnName = "mediaCreationTimestamp",
+                value = createdTimestampMillis.toString(),
+            )
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
index e8a561b..2b66491 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
@@ -18,6 +18,9 @@
 
 import com.android.systemui.communal.data.model.CommunalMediaModel
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import javax.inject.Inject
@@ -34,6 +37,7 @@
 @Inject
 constructor(
     private val mediaDataManager: MediaDataManager,
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) : CommunalMediaRepository {
 
     private val mediaDataListener =
@@ -61,7 +65,12 @@
     private val _mediaModel: MutableStateFlow<CommunalMediaModel> =
         MutableStateFlow(CommunalMediaModel.INACTIVE)
 
-    override val mediaModel: Flow<CommunalMediaModel> = _mediaModel
+    override val mediaModel: Flow<CommunalMediaModel> =
+        _mediaModel.logDiffsForTable(
+            tableLogBuffer = tableLogBuffer,
+            columnPrefix = "",
+            initialValue = CommunalMediaModel.INACTIVE,
+        )
 
     private fun updateMediaModel(data: MediaData? = null) {
         if (mediaDataManager.hasAnyMediaOrRecommendation()) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
index c2ea2e9..85aeb4d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
@@ -21,6 +21,12 @@
 import android.content.pm.UserInfo
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences
 import com.android.systemui.user.data.repository.UserRepository
@@ -59,11 +65,21 @@
     @Background private val bgDispatcher: CoroutineDispatcher,
     private val userRepository: UserRepository,
     private val userFileManager: UserFileManager,
+    @CommunalLog logBuffer: LogBuffer,
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) : CommunalPrefsRepository {
 
+    private val logger = Logger(logBuffer, "CommunalPrefsRepositoryImpl")
+
     override val isCtaDismissed: Flow<Boolean> =
         userRepository.selectedUserInfo
             .flatMapLatest(::observeCtaDismissState)
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isCtaDismissed",
+                initialValue = false,
+            )
             .stateIn(
                 scope = backgroundScope,
                 started = SharingStarted.WhileSubscribed(),
@@ -76,6 +92,8 @@
                 .edit()
                 .putBoolean(CTA_DISMISSED_STATE, true)
                 .apply()
+
+            logger.i("Dismissed CTA tile")
         }
 
     private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt
index 046aaaa..4c06e97 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt
@@ -25,6 +25,9 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -66,6 +69,7 @@
     private val userRepository: UserRepository,
     private val secureSettings: SecureSettings,
     @CommunalLog logBuffer: LogBuffer,
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) : CommunalTutorialRepository {
 
     companion object {
@@ -94,6 +98,12 @@
         settingsState
             .map { it.hubModeTutorialState }
             .filterNotNull()
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnPrefix = "",
+                columnName = "tutorialSettingState",
+                initialValue = HUB_MODE_TUTORIAL_NOT_STARTED,
+            )
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index f36547b..2ac9d05 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -54,8 +54,11 @@
         configurator: WidgetConfigurator? = null
     ) {}
 
-    /** Delete a widget by id from app widget service and the database. */
-    fun deleteWidget(widgetId: Int) {}
+    /** Delete a widget by id from the database. */
+    fun deleteWidgetFromDb(widgetId: Int) {}
+
+    /** Delete a widget by id from app widget host. */
+    fun deleteWidgetFromHost(widgetId: Int) {}
 
     /**
      * Update the order of widgets in the database.
@@ -143,9 +146,18 @@
         }
     }
 
-    override fun deleteWidget(widgetId: Int) {
+    override fun deleteWidgetFromDb(widgetId: Int) {
         bgScope.launch {
-            communalWidgetDao.deleteWidgetById(widgetId)
+            if (communalWidgetDao.deleteWidgetById(widgetId)) {
+                logger.i("Deleted widget with id $widgetId from DB .")
+            } else {
+                logger.w("Widget with id $widgetId cannot be deleted from DB.")
+            }
+        }
+    }
+
+    override fun deleteWidgetFromHost(widgetId: Int) {
+        bgScope.launch {
             appWidgetHost.deleteAppWidgetId(widgetId)
             logger.i("Deleted widget with id $widgetId.")
         }
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..75a27a2 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
@@ -33,16 +33,25 @@
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.BooleanFlowOperators.and
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
@@ -50,6 +59,8 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
 
 /** Encapsulates business-logic related to communal mode. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -57,6 +68,7 @@
 class CommunalInteractor
 @Inject
 constructor(
+    @Application applicationScope: CoroutineScope,
     private val communalRepository: CommunalRepository,
     private val widgetRepository: CommunalWidgetRepository,
     private val communalPrefsRepository: CommunalPrefsRepository,
@@ -65,8 +77,12 @@
     userRepository: UserRepository,
     keyguardInteractor: KeyguardInteractor,
     private val appWidgetHost: CommunalAppWidgetHost,
-    private val editWidgetsActivityStarter: EditWidgetsActivityStarter
+    private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
+    @CommunalLog logBuffer: LogBuffer,
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) {
+    private val logger = Logger(logBuffer, "CommunalInteractor")
+
     private val _editModeOpen = MutableStateFlow(false)
 
     /** Whether edit mode is currently open. */
@@ -85,6 +101,22 @@
                 or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming)
             )
             .distinctUntilChanged()
+            .onEach { available ->
+                logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
+                    bool1 = available
+                }
+            }
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isCommunalAvailable",
+                initialValue = false,
+            )
+            .shareIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                replay = 1,
+            )
 
     /**
      * Target scene as requested by the underlying [SceneTransitionLayout] or through
@@ -129,11 +161,36 @@
             .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 }
+        communalRepository.desiredScene
+            .map { it == CommunalSceneKey.Communal }
+            .distinctUntilChanged()
+            .onEach { showing ->
+                logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing }
+            }
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isCommunalShowing",
+                initialValue = false,
+            )
+            .shareIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                replay = 1,
+            )
 
     /**
      * Flow that emits a boolean if the communal UI is fully visible and not in transition.
@@ -146,6 +203,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)
@@ -170,9 +237,15 @@
         configurator: WidgetConfigurator?,
     ) = widgetRepository.addWidget(componentName, priority, configurator)
 
-    /** Delete a widget by id. */
-    fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
+    /**
+     * Delete a widget by id from the database. [CommunalAppWidgetHostStartable] invokes this
+     * function to manage the deletion from the database for uninstalled or user-deleted widgets,
+     * following the removal of a widget from the host.
+     */
+    fun deleteWidgetFromDb(id: Int) = widgetRepository.deleteWidgetFromDb(id)
 
+    /** Delete a widget by id from AppWidgetHost. Called when user deletes a widget from the hub */
+    fun deleteWidgetFromHost(id: Int) = widgetRepository.deleteWidgetFromHost(id)
     /**
      * Reorder the widgets.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
index 309c84e..1404ee2 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
@@ -22,6 +22,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,6 +52,7 @@
     keyguardInteractor: KeyguardInteractor,
     private val communalRepository: CommunalRepository,
     communalInteractor: CommunalInteractor,
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) {
     /** An observable for whether the tutorial is available. */
     val isTutorialAvailable: StateFlow<Boolean> =
@@ -61,6 +65,12 @@
                     isKeyguardVisible &&
                     tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
             }
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isTutorialAvailable",
+                initialValue = false,
+            )
             .stateIn(
                 scope = scope,
                 started = SharingStarted.WhileSubscribed(),
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index ebcfb8b..69d5581 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -21,6 +21,9 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.dagger.MediaModule
 import javax.inject.Inject
@@ -29,6 +32,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 
 /** The view model for communal hub in edit mode. */
 @SysUISingleton
@@ -38,21 +42,27 @@
     private val communalInteractor: CommunalInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     private val uiEventLogger: UiEventLogger,
+    @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
+
+    private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
+
     override val isEditMode = true
 
     // Only widgets are editable. The CTA tile comes last in the list and remains visible.
     override val communalContent: Flow<List<CommunalContentModel>> =
-        communalInteractor.widgetContent.map { widgets ->
-            widgets + listOf(CommunalContentModel.CtaTileInEditMode())
-        }
+        communalInteractor.widgetContent
+            .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) }
+            .onEach { models ->
+                logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
+            }
 
     private val _reorderingWidgets = MutableStateFlow(false)
 
     override val reorderingWidgets: StateFlow<Boolean>
         get() = _reorderingWidgets
 
-    override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+    override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidgetFromHost(id)
 
     override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
         communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index d7a3705..0c12841 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -21,6 +21,9 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.controls.ui.MediaHostState
@@ -37,6 +40,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
 /** The default view model used for showing the communal hub. */
@@ -48,22 +52,30 @@
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
+    @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
+
+    private val logger = Logger(logBuffer, "CommunalViewModel")
+
     @OptIn(ExperimentalCoroutinesApi::class)
     override val communalContent: Flow<List<CommunalContentModel>> =
-        tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode ->
-            if (isTutorialMode) {
-                return@flatMapLatest flowOf(communalInteractor.tutorialContent)
+        tutorialInteractor.isTutorialAvailable
+            .flatMapLatest { isTutorialMode ->
+                if (isTutorialMode) {
+                    return@flatMapLatest flowOf(communalInteractor.tutorialContent)
+                }
+                combine(
+                    communalInteractor.ongoingContent,
+                    communalInteractor.widgetContent,
+                    communalInteractor.ctaTileContent,
+                ) { ongoing, widgets, ctaTile,
+                    ->
+                    ongoing + widgets + ctaTile
+                }
             }
-            combine(
-                communalInteractor.ongoingContent,
-                communalInteractor.widgetContent,
-                communalInteractor.ctaTileContent,
-            ) { ongoing, widgets, ctaTile,
-                ->
-                ongoing + widgets + ctaTile
+            .onEach { models ->
+                logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
             }
-        }
 
     private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
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 fb9abeb..6fd0fbe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -50,7 +50,7 @@
             .launchIn(bgScope)
 
         appWidgetHost.appWidgetIdToRemove
-            .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) }
+            .onEach { appWidgetId -> communalInteractor.deleteWidgetFromDb(appWidgetId) }
             .launchIn(bgScope)
     }
 
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 54c709d..92e8153 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -32,6 +32,10 @@
 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.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 /** An Activity for editing the widgets that appear in hub mode. */
@@ -41,16 +45,21 @@
     private val communalViewModel: CommunalEditModeViewModel,
     private var windowManagerService: IWindowManager? = null,
     private val uiEventLogger: UiEventLogger,
-    private val widgetConfiguratorFactory: WidgetConfigurationController.Factory
+    private val widgetConfiguratorFactory: WidgetConfigurationController.Factory,
+    @CommunalLog logBuffer: LogBuffer,
 ) : 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"
     }
 
+    private val logger = Logger(logBuffer, "EditWidgetsActivity")
+
     private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) }
 
     private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
@@ -111,11 +120,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
+                                        )
                                     )
                                 }
                             )
@@ -146,11 +163,15 @@
 
     override fun onStart() {
         super.onStart()
+
+        logger.i("Starting the communal widget editor activity")
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN)
     }
 
     override fun onStop() {
         super.onStop()
+
+        logger.i("Stopping the communal widget editor activity")
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE)
     }
 
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/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c69c9ef..6eff792 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -585,10 +585,6 @@
     @JvmField
     val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button")
 
-    // TODO(b/287205379): Tracking bug
-    @JvmField
-    val QS_CONTAINER_GRAPH_OPTIMIZER = releasedFlag( "qs_container_graph_optimizer")
-
     /** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */
     @JvmField
     val BLUETOOTH_QS_TILE_DIALOG = releasedFlag("bluetooth_qs_tile_dialog")
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 74d7b06..4cabd70 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -83,10 +83,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/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/log/dagger/CommunalTableLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt
new file mode 100644
index 0000000..fe7dc4b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [TableLogBuffer] for communal-related logging. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class CommunalTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 3e00940..23029e6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -579,6 +579,16 @@
         return factory.create("CommunalLog", 250);
     }
 
+    /**
+     * Provides a {@link TableLogBuffer} for communal-related logs.
+     */
+    @Provides
+    @SysUISingleton
+    @CommunalTableLog
+    public static TableLogBuffer provideCommunalTableLogBuffer(TableLogBufferFactory factory) {
+        return factory.create("CommunalTableLog", 250);
+    }
+
     /** Provides a {@link LogBuffer} for display metrics related logs. */
     @Provides
     @SysUISingleton
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/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/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/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/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 84cad1d..c0afa32 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -30,8 +30,6 @@
 import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -59,18 +57,17 @@
 
 @SysUISingleton
 class NotificationsQSContainerController @Inject constructor(
-        view: NotificationsQuickSettingsContainer,
-        private val navigationModeController: NavigationModeController,
-        private val overviewProxyService: OverviewProxyService,
-        private val shadeHeaderController: ShadeHeaderController,
-        private val shadeInteractor: ShadeInteractor,
-        private val fragmentService: FragmentService,
-        @Main private val delayableExecutor: DelayableExecutor,
-        private val featureFlags: FeatureFlags,
-        private val
-            notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
-        private val splitShadeStateController: SplitShadeStateController,
-        private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
+    view: NotificationsQuickSettingsContainer,
+    private val navigationModeController: NavigationModeController,
+    private val overviewProxyService: OverviewProxyService,
+    private val shadeHeaderController: ShadeHeaderController,
+    private val shadeInteractor: ShadeInteractor,
+    private val fragmentService: FragmentService,
+    @Main private val delayableExecutor: DelayableExecutor,
+    private val
+    notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
+    private val splitShadeStateController: SplitShadeStateController,
+    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
     private var splitShadeEnabled = false
@@ -133,9 +130,6 @@
         isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
 
         mView.setStackScroller(notificationStackScrollLayoutController.getView())
-        if (featureFlags.isEnabled(Flags.QS_CONTAINER_GRAPH_OPTIMIZER)){
-            mView.enableGraphOptimization()
-        }
     }
 
     public override fun onViewAttached() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index de3d16a..25e558e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -32,10 +32,10 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.constraintlayout.widget.ConstraintSet;
 
-import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
-import com.android.systemui.res.R;
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
 
 import java.util.ArrayList;
@@ -73,6 +73,7 @@
 
     public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
+        setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH);
     }
 
     @Override
@@ -180,10 +181,6 @@
         super.dispatchDraw(canvas);
     }
 
-    void enableGraphOptimization() {
-        setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH);
-    }
-
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev,
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/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 3c4ca44..b0192c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -22,6 +22,7 @@
 import android.media.projection.MediaProjectionManager;
 import android.os.Handler;
 import android.os.Trace;
+import android.service.notification.StatusBarNotification;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.SysUISingleton;
@@ -46,8 +47,9 @@
                 public void onStart(MediaProjectionInfo info) {
                     Trace.beginSection(
                             "SNPC.onProjectionStart");
-                    mProjection = info;
-                    mListeners.forEach(Runnable::run);
+                    // Only enable sensitive content protection if sharing full screen
+                    // Launch cookie only set (non-null) if sharing single app/task
+                    updateProjectionState((info.getLaunchCookie() == null) ? info : null);
                     Trace.endSection();
                 }
 
@@ -55,10 +57,22 @@
                 public void onStop(MediaProjectionInfo info) {
                     Trace.beginSection(
                             "SNPC.onProjectionStop");
-                    mProjection = null;
-                    mListeners.forEach(Runnable::run);
+                    updateProjectionState(null);
                     Trace.endSection();
                 }
+
+                private void updateProjectionState(MediaProjectionInfo info) {
+                    // capture previous state
+                    boolean wasSensitive = isSensitiveStateActive();
+
+                    // update internal state
+                    mProjection = info;
+
+                    // if either previous or new state is sensitive, notify listeners.
+                    if (wasSensitive || isSensitiveStateActive()) {
+                        mListeners.forEach(Runnable::run);
+                    }
+                }
             };
 
     @Inject
@@ -86,7 +100,6 @@
     public boolean isSensitiveStateActive() {
         // TODO(b/316955558): Add disabled by developer option
         // TODO(b/316955306): Add feature exemption for sysui and bug handlers
-        // TODO(b/316955346): Add feature exemption for single app screen sharing
         return mProjection != null;
     }
 
@@ -96,9 +109,18 @@
             return false;
         }
 
+        MediaProjectionInfo projection = mProjection;
+        if (projection == null) {
+            return false;
+        }
+
         // Exempt foreground service notifications from protection in effort to keep screen share
         // stop actions easily accessible
-        // TODO(b/316955208): Exempt FGS notifications only for app that started projection
-        return !entry.getSbn().getNotification().isFgsOrUij();
+        StatusBarNotification sbn = entry.getSbn();
+        if (sbn.getNotification().isFgsOrUij()) {
+            return !sbn.getPackageName().equals(projection.getPackageName());
+        }
+
+        return true;
     }
 }
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/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/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index 8d5e55a..ff1daea 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.dagger
 
+import android.content.Context
 import android.media.AudioManager
 import com.android.settingslib.volume.data.repository.AudioRepository
 import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
@@ -35,10 +36,12 @@
 
         @Provides
         fun provideAudioRepository(
+            @Application context: Context,
             audioManager: AudioManager,
             @Background coroutineContext: CoroutineContext,
             @Application coroutineScope: CoroutineScope,
-        ): AudioRepository = AudioRepositoryImpl(audioManager, coroutineContext, coroutineScope)
+        ): AudioRepository =
+            AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope)
 
         @Provides
         fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor =
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 8299acb..375ebe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -51,6 +51,7 @@
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 import android.view.animation.AccelerateInterpolator;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -79,6 +80,7 @@
 
 @LargeTest
 @RunWith(AndroidTestingRunner.class)
+@FlakyTest(bugId = 308501761)
 public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
 
     @Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 9c5cd71..20dd913 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -77,6 +77,18 @@
         }
 
     @Test
+    fun deleteWidget_notInDb_returnsFalse() =
+        testScope.runTest {
+            val (widgetId, provider, priority) = widgetInfo1
+            communalWidgetDao.addWidget(
+                widgetId = widgetId,
+                provider = provider,
+                priority = priority,
+            )
+            assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse()
+        }
+
+    @Test
     fun addWidget_emitsActiveWidgetsInDb(): Unit =
         testScope.runTest {
             val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index 437a35f..e3c4c28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -475,10 +475,7 @@
 
         // Then the device name is the PhoneMediaDevice string
         val data = captureDeviceData(KEY)
-        assertThat(data.name)
-            .isEqualTo(
-                context.getString(com.android.settingslib.R.string.media_transfer_this_device_name)
-            )
+        assertThat(data.name).isEqualTo(PhoneMediaDevice.getMediaTransferThisDeviceName(context))
     }
 
     @Test
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 239bf65..edba902 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
@@ -30,6 +30,7 @@
 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.dream.MediaDreamComplication
@@ -52,8 +53,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertNotNull
@@ -106,8 +105,7 @@
     private lateinit var dreamOverlayCallback:
         ArgumentCaptor<(DreamOverlayStateController.Callback)>
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val testScope = kosmos.testScope
     private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var isQsBypassingShade: MutableStateFlow<Boolean>
     private lateinit var mediaFrame: ViewGroup
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/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index a6e240b..0831971 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -44,6 +44,7 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.BeforeClass
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -51,6 +52,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@Ignore("b/323053208")
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
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/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
index 697b05a..c226121 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -28,8 +28,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.fragments.FragmentHostManager
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
@@ -94,7 +92,6 @@
 
     lateinit var underTest: NotificationsQSContainerController
 
-    private lateinit var featureFlags: FakeFeatureFlags
     private lateinit var navigationModeCallback: ModeChangedListener
     private lateinit var taskbarVisibilityCallback: OverviewProxyListener
     private lateinit var windowInsetsCallback: Consumer<WindowInsets>
@@ -106,7 +103,6 @@
         MockitoAnnotations.initMocks(this)
         fakeSystemClock = FakeSystemClock()
         delayableExecutor = FakeExecutor(fakeSystemClock)
-        featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, false) }
         mContext.ensureTestableResources()
         whenever(view.context).thenReturn(mContext)
         whenever(view.resources).thenReturn(mContext.resources)
@@ -123,7 +119,6 @@
                 shadeInteractor,
                 fragmentService,
                 delayableExecutor,
-                featureFlags,
                 notificationStackScrollLayoutController,
                 ResourcesSplitShadeStateController(),
                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
@@ -536,7 +531,6 @@
                 shadeInteractor,
                 fragmentService,
                 delayableExecutor,
-                featureFlags,
                 notificationStackScrollLayoutController,
                 ResourcesSplitShadeStateController(),
                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
index e66251a..c326350 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -28,8 +28,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.fragments.FragmentHostManager
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
@@ -91,7 +89,6 @@
 
     lateinit var underTest: NotificationsQSContainerController
 
-    private lateinit var featureFlags: FakeFeatureFlags
     private lateinit var navigationModeCallback: ModeChangedListener
     private lateinit var taskbarVisibilityCallback: OverviewProxyListener
     private lateinit var windowInsetsCallback: Consumer<WindowInsets>
@@ -104,7 +101,6 @@
         fakeSystemClock = FakeSystemClock()
         delayableExecutor = FakeExecutor(fakeSystemClock)
         mSetFlagsRule.enableFlags(KeyguardShadeMigrationNssl.FLAG_NAME)
-        featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, true) }
         mContext.ensureTestableResources()
         whenever(view.context).thenReturn(mContext)
         whenever(view.resources).thenReturn(mContext.resources)
@@ -122,7 +118,6 @@
                 shadeInteractor,
                 fragmentService,
                 delayableExecutor,
-                featureFlags,
                 notificationStackScrollLayoutController,
                 ResourcesSplitShadeStateController(),
                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
@@ -513,7 +508,6 @@
                 shadeInteractor,
                 fragmentService,
                 delayableExecutor,
-                featureFlags,
                 notificationStackScrollLayoutController,
                 ResourcesSplitShadeStateController(),
                 largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
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/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/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index cd5d5ed..9d53e7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy
 
+import android.app.ActivityOptions
 import android.app.Notification
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
@@ -69,6 +70,8 @@
         MockitoAnnotations.initMocks(this)
         mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
 
+        setShareFullScreen()
+
         controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
 
         // Obtain useful MediaProjectionCallback
@@ -195,6 +198,14 @@
     }
 
     @Test
+    fun isSensitiveStateActive_projectionActive_singleActivity_false() {
+        setShareSingleApp()
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        assertFalse(controller.isSensitiveStateActive)
+    }
+
+    @Test
     fun shouldProtectNotification_projectionInactive_false() {
         val notificationEntry = mock(NotificationEntry::class.java)
 
@@ -202,30 +213,74 @@
     }
 
     @Test
-    fun shouldProtectNotification_projectionActive_fgsNotification_false() {
+    fun shouldProtectNotification_projectionActive_singleActivity_false() {
+        setShareSingleApp()
         mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
 
-        val notificationEntry = mock(NotificationEntry::class.java)
-        val sbn = mock(StatusBarNotification::class.java)
-        val notification = mock(Notification::class.java)
-        `when`(notificationEntry.sbn).thenReturn(sbn)
-        `when`(sbn.notification).thenReturn(notification)
-        `when`(notification.isFgsOrUij).thenReturn(true)
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
 
         assertFalse(controller.shouldProtectNotification(notificationEntry))
     }
 
     @Test
+    fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
     fun shouldProtectNotification_projectionActive_notFgsNotification_true() {
         mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
 
+        val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    private fun setShareFullScreen() {
+        `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
+        `when`(mediaProjectionInfo.launchCookie).thenReturn(null)
+    }
+
+    private fun setShareSingleApp() {
+        `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
+        `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie())
+    }
+
+    private fun setupNotificationEntry(
+        packageName: String,
+        isFgs: Boolean = false
+    ): NotificationEntry {
         val notificationEntry = mock(NotificationEntry::class.java)
         val sbn = mock(StatusBarNotification::class.java)
         val notification = mock(Notification::class.java)
         `when`(notificationEntry.sbn).thenReturn(sbn)
+        `when`(sbn.packageName).thenReturn(packageName)
         `when`(sbn.notification).thenReturn(notification)
-        `when`(notification.isFgsOrUij).thenReturn(false)
+        `when`(notification.isFgsOrUij).thenReturn(isFgs)
 
-        assertTrue(controller.shouldProtectNotification(notificationEntry))
+        return notificationEntry
+    }
+
+    private fun setupFgsNotificationEntry(packageName: String): NotificationEntry {
+        return setupNotificationEntry(packageName, /* isFgs= */ true)
+    }
+
+    companion object {
+        private const val TEST_PROJECTION_PACKAGE_NAME =
+            "com.android.systemui.statusbar.policy.projectionpackage"
+        private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage"
     }
 }
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 0a3c2d9..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)
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 fab64e3..7301404 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,7 +36,7 @@
         }
     }
 
-    override fun deleteWidget(widgetId: Int) {
+    override fun deleteWidgetFromDb(widgetId: Int) {
         if (_communalWidgets.value.none { it.appWidgetId == widgetId }) {
             return
         }
@@ -44,6 +44,10 @@
         _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId }
     }
 
+    override fun deleteWidgetFromHost(widgetId: Int) {
+        deleteWidgetFromDb(widgetId)
+    }
+
     private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
         _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index c818e9c..c47f020 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -24,12 +24,15 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.smartspace.data.repository.smartspaceRepository
 import com.android.systemui.user.data.repository.userRepository
 import com.android.systemui.util.mockito.mock
 
 val Kosmos.communalInteractor by Fixture {
     CommunalInteractor(
+        applicationScope = applicationCoroutineScope,
         communalRepository = communalRepository,
         widgetRepository = communalWidgetRepository,
         mediaRepository = communalMediaRepository,
@@ -39,6 +42,8 @@
         appWidgetHost = mock(),
         keyguardInteractor = keyguardInteractor,
         editWidgetsActivityStarter = editWidgetsActivityStarter,
+        logBuffer = logcatLogBuffer("CommunalInteractor"),
+        tableLogBuffer = mock(),
     )
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
index adaea7c..9776b43 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.mockito.mock
 
 val Kosmos.communalTutorialInteractor by
     Kosmos.Fixture {
@@ -30,5 +31,6 @@
             keyguardInteractor = keyguardInteractor,
             communalRepository = communalRepository,
             communalInteractor = communalInteractor,
+            tableLogBuffer = mock(),
         )
     }
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 083de10..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
@@ -43,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
 
@@ -80,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/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/ravenwood/README-ravenwood+mockito.md b/ravenwood/README-ravenwood+mockito.md
deleted file mode 100644
index 6adb6144..0000000
--- a/ravenwood/README-ravenwood+mockito.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Ravenwood and Mockito
-
-Last update: 2023-11-13
-
-- As of 2023-11-13, `external/mockito` is based on version 2.x.
-- Mockito didn't support static mocking before 3.4.0.
-  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
-
-- Latest Mockito is 5.*. According to https://github.com/mockito/mockito:
-  `Mockito 3 does not introduce any breaking API changes, but now requires Java 8 over Java 6 for Mockito 2. Mockito 4 removes deprecated API. Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.`
-
-- Mockito now supports Android natively.
-  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.1
-  - But it's unclear at this point to omakoto@ how the `mockito-android` module is built.
-
-- Potential plan:
-  - Ideal option:
-    - If we can update `external/mockito`, that'd be great, but it may not work because
-      Mockito has removed the deprecated APIs.
-  - Second option:
-    - Import the latest mockito as `external/mockito-new`, and require ravenwood
-      to use this one.
-    - The latest mockito needs be exposed to all of 1) device tests, 2) host tests, and 3) ravenwood tests.
-    - This probably will require the latest `bytebuddy` and `objenesis`.
\ No newline at end of file
diff --git a/ravenwood/list-ravenwood-tests.sh b/ravenwood/list-ravenwood-tests.sh
new file mode 100755
index 0000000..fb9b823
--- /dev/null
+++ b/ravenwood/list-ravenwood-tests.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# 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.
+
+# List all the ravenwood test modules.
+
+jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json"
diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp
index 4135022..a74bca4 100644
--- a/ravenwood/mockito/Android.bp
+++ b/ravenwood/mockito/Android.bp
@@ -7,16 +7,6 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-// Ravenwood tests run on the hostside, so we need mockit of the host variant.
-// But we need to use it in modules of the android variant, so we "wash" the variant with it.
-java_host_for_device {
-    name: "mockito_ravenwood",
-    libs: [
-        "mockito",
-        "objenesis",
-    ],
-}
-
 android_ravenwood_test {
     name: "RavenwoodMockitoTest",
 
@@ -26,8 +16,6 @@
     static_libs: [
         "junit",
         "truth",
-
-        "mockito_ravenwood",
     ],
     libs: [
         "android.test.mock",
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
index 364a86a..1284d64 100644
--- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
@@ -22,6 +22,7 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Parcel;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import org.junit.Rule;
@@ -78,4 +79,13 @@
 
         assertThat(object.getPackageName()).isEqualTo("android");
     }
+
+    @Test
+    public void testMockFinalClass() {
+        var object = mock(Parcel.class);
+
+        when(object.readInt()).thenReturn(123);
+
+        assertThat(object.readInt()).isEqualTo(123);
+    }
 }
diff --git a/ravenwood/run-ravenwood-tests.sh b/ravenwood/run-ravenwood-tests.sh
new file mode 100755
index 0000000..3f4b8a7
--- /dev/null
+++ b/ravenwood/run-ravenwood-tests.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# 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.
+
+# Run all the ravenwood tests.
+
+# "echo" is to remove the newlines
+all_tests=$(echo $(${0%/*}/list-ravenwood-tests.sh) )
+
+echo "Running tests: $all_tests"
+atest $all_tests
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 63784ba..44144f8 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -5725,10 +5725,10 @@
     }
 
     @Override
-    public void attachAccessibilityOverlayToDisplay_enforcePermission(
+    public void attachAccessibilityOverlayToDisplay(
             int displayId, SurfaceControl sc) {
         mContext.enforceCallingPermission(
-                INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission");
+                INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay");
         mMainHandler.sendMessage(
                 obtainMessage(
                         AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal,
diff --git a/services/autofill/features.aconfig b/services/autofill/features.aconfig
index 727721d..532db12 100644
--- a/services/autofill/features.aconfig
+++ b/services/autofill/features.aconfig
@@ -12,4 +12,11 @@
     namespace: "autofill"
     description: "Guards against Autofill-Credman integration phase 2"
     bug: "320730001"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "autofill_credman_dev_integration"
+    namespace: "autofill"
+    description: "Guards against Autofill-Credman Phase1 developer integration via new APIs"
+    bug: "320730001"
+}
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index cd8be33..82d9377 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import static android.app.Flags.modesApi;
+import static android.app.Flags.enableNightModeCache;
 import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
 import static android.app.UiModeManager.DEFAULT_PRIORITY;
 import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF;
@@ -135,7 +136,23 @@
     private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
 
     private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
-    private int mNightMode = UiModeManager.MODE_NIGHT_NO;
+
+    private final NightMode mNightMode = new NightMode(){
+        private int mNightModeValue = UiModeManager.MODE_NIGHT_NO;
+
+        @Override
+        public int get() {
+            return mNightModeValue;
+        }
+
+        @Override
+        public void set(int mode) {
+            mNightModeValue = mode;
+            if (enableNightModeCache()) {
+                UiModeManager.invalidateNightModeCache();
+            }
+        }
+    };
     private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
     private int mAttentionModeThemeOverlay = UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF;
     private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0);
@@ -297,7 +314,7 @@
         @Override
         public void onTwilightStateChanged(@Nullable TwilightState state) {
             synchronized (mLock) {
-                if (mNightMode == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) {
+                if (mNightMode.get() == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) {
                     if (shouldApplyAutomaticChangesImmediately()) {
                         updateLocked(0, 0);
                     } else {
@@ -392,7 +409,7 @@
 
     private void updateSystemProperties() {
         int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE,
-                mNightMode, 0);
+                mNightMode.get(), 0);
         if (mode == MODE_NIGHT_AUTO || mode == MODE_NIGHT_CUSTOM) {
             mode = MODE_NIGHT_YES;
         }
@@ -412,7 +429,7 @@
     @Override
     public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
         mCurrentUser = to.getUserIdentifier();
-        if (mNightMode == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier());
+        if (mNightMode.get() == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier());
         getContext().getContentResolver().unregisterContentObserver(mSetupWizardObserver);
         verifySetupWizardCompleted();
         synchronized (mLock) {
@@ -471,12 +488,12 @@
         verifySetupWizardCompleted();
 
         final Resources res = context.getResources();
+        mNightMode.set(res.getInteger(
+                com.android.internal.R.integer.config_defaultNightMode));
         mStartDreamImmediatelyOnDock = res.getBoolean(
                 com.android.internal.R.bool.config_startDreamImmediatelyOnDock);
         mDreamsDisabledByAmbientModeSuppression = res.getBoolean(
                 com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
-        mNightMode = res.getInteger(
-                com.android.internal.R.integer.config_defaultNightMode);
         mDefaultUiModeType = res.getInteger(
                 com.android.internal.R.integer.config_defaultUiModeType);
         mCarModeKeepsScreenOn = (res.getInteger(
@@ -510,7 +527,7 @@
     private final BroadcastReceiver mOnShutdown = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (mNightMode == MODE_NIGHT_AUTO) {
+            if (mNightMode.get() == MODE_NIGHT_AUTO) {
                 persistComputedNightMode(mCurrentUser);
             }
         }
@@ -585,7 +602,7 @@
     }
 
     private void updateCustomTimeLocked() {
-        if (mNightMode != MODE_NIGHT_CUSTOM) return;
+        if (mNightMode.get() != MODE_NIGHT_CUSTOM) return;
         if (shouldApplyAutomaticChangesImmediately()) {
             updateLocked(0, 0);
         } else {
@@ -606,9 +623,9 @@
             return;
         }
         if (mSetupWizardComplete) {
-            mNightMode = Secure.getIntForUser(context.getContentResolver(),
+            mNightMode.set(Secure.getIntForUser(context.getContentResolver(),
                     Secure.UI_NIGHT_MODE, res.getInteger(
-                            com.android.internal.R.integer.config_defaultNightMode), userId);
+                            com.android.internal.R.integer.config_defaultNightMode), userId));
             mNightModeCustomType = Secure.getIntForUser(context.getContentResolver(),
                     Secure.UI_NIGHT_MODE_CUSTOM_TYPE, MODE_NIGHT_CUSTOM_TYPE_UNKNOWN, userId);
                     mOverrideNightModeOn = Secure.getIntForUser(context.getContentResolver(),
@@ -623,7 +640,7 @@
                     Secure.getLongForUser(context.getContentResolver(),
                             Secure.DARK_THEME_CUSTOM_END_TIME,
                             DEFAULT_CUSTOM_NIGHT_END_TIME.toNanoOfDay() / 1000L, userId) * 1000);
-            if (mNightMode == MODE_NIGHT_AUTO) {
+            if (mNightMode.get() == MODE_NIGHT_AUTO) {
                 mComputedNightMode = Secure.getIntForUser(context.getContentResolver(),
                         Secure.UI_NIGHT_MODE_LAST_COMPUTED, 0, userId) != 0;
             }
@@ -834,21 +851,23 @@
             final long ident = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    if (mNightMode != mode || mNightModeCustomType != customModeType) {
-                        if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) {
+                    if (mNightMode.get() != mode || mNightModeCustomType != customModeType) {
+                        if (mNightMode.get() == MODE_NIGHT_AUTO
+                                || mNightMode.get() == MODE_NIGHT_CUSTOM) {
                             unregisterDeviceInactiveListenerLocked();
                             cancelCustomAlarm();
                         }
                         mNightModeCustomType = mode == MODE_NIGHT_CUSTOM
                                 ? customModeType
                                 : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
-                        mNightMode = mode;
+                        mNightMode.set(mode);
                         //deactivates AttentionMode if user toggles DarkTheme
                         mAttentionModeThemeOverlay = MODE_ATTENTION_THEME_OVERLAY_OFF;
                         resetNightModeOverrideLocked();
                         persistNightMode(user);
                         // on screen off will update configuration instead
-                        if ((mNightMode != MODE_NIGHT_AUTO && mNightMode != MODE_NIGHT_CUSTOM)
+                        if ((mNightMode.get() != MODE_NIGHT_AUTO
+                                && mNightMode.get() != MODE_NIGHT_CUSTOM)
                                 || shouldApplyAutomaticChangesImmediately()) {
                             unregisterDeviceInactiveListenerLocked();
                             updateLocked(0, 0);
@@ -865,7 +884,7 @@
         @Override
         public int getNightMode() {
             synchronized (mLock) {
-                return mNightMode;
+                return mNightMode.get();
             }
         }
 
@@ -999,18 +1018,19 @@
             synchronized (mLock) {
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) {
+                    if (mNightMode.get() == MODE_NIGHT_AUTO
+                            || mNightMode.get() == MODE_NIGHT_CUSTOM) {
                         unregisterDeviceInactiveListenerLocked();
                         mOverrideNightModeOff = !active;
                         mOverrideNightModeOn = active;
                         mOverrideNightModeUser = mCurrentUser;
                         persistNightModeOverrides(mCurrentUser);
-                    } else if (mNightMode == UiModeManager.MODE_NIGHT_NO
+                    } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_NO
                             && active) {
-                        mNightMode = UiModeManager.MODE_NIGHT_YES;
-                    } else if (mNightMode == UiModeManager.MODE_NIGHT_YES
+                        mNightMode.set(UiModeManager.MODE_NIGHT_YES);
+                    } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_YES
                             && !active) {
-                        mNightMode = UiModeManager.MODE_NIGHT_NO;
+                        mNightMode.set(UiModeManager.MODE_NIGHT_NO);
                     }
                     updateConfigurationLocked();
                     applyConfigurationExternallyLocked();
@@ -1414,7 +1434,7 @@
 
     private void onCustomTimeUpdated(int user) {
         persistNightMode(user);
-        if (mNightMode != MODE_NIGHT_CUSTOM) return;
+        if (mNightMode.get() != MODE_NIGHT_CUSTOM) return;
         if (shouldApplyAutomaticChangesImmediately()) {
             unregisterDeviceInactiveListenerLocked();
             updateLocked(0, 0);
@@ -1431,8 +1451,8 @@
 
             pw.print(" mStartDreamImmediatelyOnDock="); pw.print(mStartDreamImmediatelyOnDock);
 
-            pw.print("  mNightMode="); pw.print(mNightMode); pw.print(" (");
-            pw.print(Shell.nightModeToStr(mNightMode, mNightModeCustomType)); pw.print(") ");
+            pw.print("  mNightMode="); pw.print(mNightMode.get()); pw.print(" (");
+            pw.print(Shell.nightModeToStr(mNightMode.get(), mNightModeCustomType)); pw.print(") ");
             pw.print(" mOverrideOn/Off="); pw.print(mOverrideNightModeOn);
             pw.print("/"); pw.print(mOverrideNightModeOff);
             pw.print("  mAttentionModeThemeOverlay="); pw.print(mAttentionModeThemeOverlay);
@@ -1623,7 +1643,7 @@
         // Only persist setting if not in car mode
         if (mCarModeEnabled || mCar) return;
         Secure.putIntForUser(getContext().getContentResolver(),
-                Secure.UI_NIGHT_MODE, mNightMode, user);
+                Secure.UI_NIGHT_MODE, mNightMode.get(), user);
         Secure.putLongForUser(getContext().getContentResolver(),
                 Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mNightModeCustomType, user);
         Secure.putLongForUser(getContext().getContentResolver(),
@@ -1659,11 +1679,11 @@
             uiMode = Configuration.UI_MODE_TYPE_VR_HEADSET;
         }
 
-        if (mNightMode == MODE_NIGHT_YES || mNightMode == UiModeManager.MODE_NIGHT_NO) {
-            updateComputedNightModeLocked(mNightMode == MODE_NIGHT_YES);
+        if (mNightMode.get() == MODE_NIGHT_YES || mNightMode.get() == UiModeManager.MODE_NIGHT_NO) {
+            updateComputedNightModeLocked(mNightMode.get() == MODE_NIGHT_YES);
         }
 
-        if (mNightMode == MODE_NIGHT_AUTO) {
+        if (mNightMode.get() == MODE_NIGHT_AUTO) {
             boolean activateNightMode = mComputedNightMode;
             if (mTwilightManager != null) {
                 mTwilightManager.registerListener(mTwilightListener, mHandler);
@@ -1677,7 +1697,7 @@
             }
         }
 
-        if (mNightMode == MODE_NIGHT_CUSTOM) {
+        if (mNightMode.get() == MODE_NIGHT_CUSTOM) {
             if (mNightModeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) {
                 updateComputedNightModeLocked(mLastBedtimeRequestedNightMode);
             } else {
@@ -2010,7 +2030,7 @@
 
     private void updateComputedNightModeLocked(boolean activate) {
         boolean newComputedValue = activate;
-        if (mNightMode != MODE_NIGHT_YES && mNightMode != UiModeManager.MODE_NIGHT_NO) {
+        if (mNightMode.get() != MODE_NIGHT_YES && mNightMode.get() != UiModeManager.MODE_NIGHT_NO) {
             if (mOverrideNightModeOn && !newComputedValue) {
                 newComputedValue = true;
             } else if (mOverrideNightModeOff && newComputedValue) {
@@ -2029,7 +2049,7 @@
             mComputedNightMode = newComputedValue;
         }
 
-        if (mNightMode != MODE_NIGHT_AUTO || (mTwilightManager != null
+        if (mNightMode.get() != MODE_NIGHT_AUTO || (mTwilightManager != null
                 && mTwilightManager.getLastTwilightState() != null)) {
             resetNightModeOverrideLocked();
         }
@@ -2279,4 +2299,13 @@
             Sandman.startDreamWhenDockedIfAppropriate(context);
         }
     }
+
+    /**
+     * Interface to contain the value for system night mode. We make the night mode accessible
+     * through this class to ensure that the reassignment of this value invalidates the cache.
+     */
+    private interface NightMode {
+        int get();
+        void set(int mode);
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index cd295b5..f1eea72 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -2820,6 +2820,10 @@
         return mDeviceInventory.isBluetoothAudioDeviceCategoryFixed(address);
     }
 
+    /*package*/ boolean isSADevice(AdiDeviceState deviceState) {
+        return mAudioService.isSADevice(deviceState);
+    }
+
     //------------------------------------------------
     // for testing purposes only
     void clearDeviceInventory() {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 690c37a..102a960 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -307,9 +307,12 @@
                             && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) {
                         continue;
                     }
-                    ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
-                    ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
-                    ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                    if (mDeviceBroker.isSADevice(updatedDevice)
+                            == mDeviceBroker.isSADevice(ads2)) {
+                        ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                        ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                        ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                    }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
                     mDeviceBroker.postUpdatedAdiDeviceState(ads2);
@@ -325,9 +328,12 @@
                             && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) {
                         continue;
                     }
-                    ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
-                    ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
-                    ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                    if (mDeviceBroker.isSADevice(updatedDevice)
+                            == mDeviceBroker.isSADevice(ads2)) {
+                        ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                        ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                        ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                    }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
                     mDeviceBroker.postUpdatedAdiDeviceState(ads2);
@@ -348,10 +354,11 @@
                     || !updatedDevice.getDeviceAddress().equals(ads.getDeviceAddress())) {
                 continue;
             }
-
-            ads.setHasHeadTracker(updatedDevice.hasHeadTracker());
-            ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
-            ads.setSAEnabled(updatedDevice.isSAEnabled());
+            if (mDeviceBroker.isSADevice(updatedDevice) == mDeviceBroker.isSADevice(ads)) {
+                ads.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                ads.setSAEnabled(updatedDevice.isSAEnabled());
+            }
             ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
             mDeviceBroker.postUpdatedAdiDeviceState(ads);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 9f7c07e..bbbba26 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7855,7 +7855,7 @@
 
         sDeviceLogger.enqueue(new EventLogger.StringEvent("BlutoothActiveDeviceChanged for "
                 + BluetoothProfile.getProfileName(profile) + ", device update " + previousDevice
-                + " -> " + newDevice));
+                + " -> " + newDevice).printLog(TAG));
         AudioDeviceBroker.BtDeviceChangedData data =
                 new AudioDeviceBroker.BtDeviceChangedData(newDevice, previousDevice, info,
                         "AudioService");
@@ -10638,6 +10638,10 @@
         mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect);
     }
 
+    /*package*/ boolean isSADevice(AdiDeviceState deviceState) {
+        return mSpatializerHelper.isSADevice(deviceState);
+    }
+
     private boolean isBluetoothPrividged() {
         if (!bluetoothMacAddressAnonymization()) {
             return true;
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 8428f12..8d76731 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -813,7 +813,7 @@
         return false;
     }
 
-    private boolean isSADevice(AdiDeviceState deviceState) {
+    /*package*/ boolean isSADevice(AdiDeviceState deviceState) {
         return deviceState.getDeviceType() == getCanonicalDeviceType(deviceState.getDeviceType(),
                 deviceState.getInternalDeviceType()) && isDeviceCompatibleWithSpatializationModes(
                 deviceState.getAudioDeviceAttributes());
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/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 93addcd..e930627 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -872,14 +872,15 @@
     }
 
     @VisibleForTesting
-    void performTraversalInternal(SurfaceControl.Transaction t) {
+    void performTraversalInternal(SurfaceControl.Transaction t,
+            SparseArray<SurfaceControl.Transaction> displayTransactions) {
         synchronized (mSyncRoot) {
             if (!mPendingTraversal) {
                 return;
             }
             mPendingTraversal = false;
 
-            performTraversalLocked(t);
+            performTraversalLocked(t, displayTransactions);
         }
 
         // List is self-synchronized copy-on-write.
@@ -2593,7 +2594,8 @@
         }
     }
 
-    private void performTraversalLocked(SurfaceControl.Transaction t) {
+    private void performTraversalLocked(SurfaceControl.Transaction t,
+            SparseArray<SurfaceControl.Transaction> displayTransactions) {
         // Clear all viewports before configuring displays so that we can keep
         // track of which ones we have configured.
         clearViewportsLocked();
@@ -2601,9 +2603,11 @@
         // Configure each display device.
         mLogicalDisplayMapper.forEachLocked((LogicalDisplay display) -> {
             final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+            final SurfaceControl.Transaction displayTransaction =
+                    displayTransactions.get(display.getDisplayIdLocked(), t);
             if (device != null) {
-                configureDisplayLocked(t, device);
-                device.performTraversalLocked(t);
+                configureDisplayLocked(displayTransaction, device);
+                device.performTraversalLocked(displayTransaction);
             }
         });
 
@@ -4680,8 +4684,9 @@
         }
 
         @Override
-        public void performTraversal(SurfaceControl.Transaction t) {
-            performTraversalInternal(t);
+        public void performTraversal(SurfaceControl.Transaction t,
+                SparseArray<SurfaceControl.Transaction> displayTransactions) {
+            performTraversalInternal(t, displayTransactions);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f031b7b..2ea662c 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3185,6 +3185,24 @@
                 }
             }
         }
+
+        if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
+            String ime = SecureSettingsWrapper.getString(
+                    Settings.Secure.DEFAULT_INPUT_METHOD, null, mSettings.getUserId());
+            String defaultDeviceIme = SecureSettingsWrapper.getString(
+                    Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, mSettings.getUserId());
+            if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Current input method " + ime + " differs from the stored default"
+                            + " device input method for user " + mSettings.getUserId()
+                            + " - restoring " + defaultDeviceIme);
+                }
+                SecureSettingsWrapper.putString(
+                        Settings.Secure.DEFAULT_INPUT_METHOD, defaultDeviceIme,
+                        mSettings.getUserId());
+            }
+        }
+
         // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and
         // ENABLED_INPUT_METHODS is taking care of keeping them correctly in
         // sync, so we will never have a DEFAULT_INPUT_METHOD that is not
@@ -3659,6 +3677,15 @@
             Slog.e(TAG, "windowToken cannot be null.");
             return InputBindResult.NULL;
         }
+        // The user represented by userId, must be running.
+        if (!mUserManagerInternal.isUserRunning(userId)) {
+            // There is a chance that we hit here because of race condition. Let's just
+            // return an error code instead of crashing the caller process, which at
+            // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an
+            // important process.
+            Slog.w(TAG, "User #" + userId + " is not running.");
+            return InputBindResult.INVALID_USER;
+        }
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "IMMS.startInputOrWindowGainedFocus");
@@ -3666,20 +3693,43 @@
                     "InputMethodManagerService#startInputOrWindowGainedFocus");
             final InputBindResult result;
             synchronized (ImfLock.class) {
+                // If the system is not yet ready, we shouldn't be running third party code.
                 if (!mSystemReady) {
-                    // If the system is not yet ready, we shouldn't be running third arty code.
                     return new InputBindResult(
                             InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
                             null /* method */, null /* accessibilitySessions */, null /* channel */,
                             getSelectedMethodIdLocked(), getSequenceNumberLocked(),
                             false /* isInputMethodSuppressingSpellChecker */);
                 }
+                final ClientState cs = mClientController.getClient(client.asBinder());
+                if (cs == null) {
+                    throw new IllegalArgumentException("Unknown client " + client.asBinder());
+                }
                 final long ident = Binder.clearCallingIdentity();
                 try {
+                    // Verify if IMMS is in the process of switching user.
+                    if (mUserSwitchHandlerTask != null) {
+                        // There is already an on-going pending user switch task.
+                        final int nextUserId = mUserSwitchHandlerTask.mToUserId;
+                        if (userId == nextUserId) {
+                            scheduleSwitchUserTaskLocked(userId, cs.mClient);
+                            return InputBindResult.USER_SWITCHING;
+                        }
+                        final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds(
+                                mSettings.getUserId(), false /* enabledOnly */);
+                        for (int profileId : profileIdsWithDisabled) {
+                            if (profileId == userId) {
+                                scheduleSwitchUserTaskLocked(userId, cs.mClient);
+                                return InputBindResult.USER_SWITCHING;
+                            }
+                        }
+                        return InputBindResult.INVALID_USER;
+                    }
+
                     result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
                             client, windowToken, startInputFlags, softInputMode, windowFlags,
                             editorInfo, inputConnection, remoteAccessibilityInputConnection,
-                            unverifiedTargetSdkVersion, userId, imeDispatcher);
+                            unverifiedTargetSdkVersion, userId, imeDispatcher, cs);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -3708,7 +3758,7 @@
             IRemoteInputConnection inputContext,
             @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) {
         if (DEBUG) {
             Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason="
                     + InputMethodDebug.startInputReasonToString(startInputReason)
@@ -3721,23 +3771,9 @@
                     + " windowFlags=#" + Integer.toHexString(windowFlags)
                     + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion
                     + " userId=" + userId
-                    + " imeDispatcher=" + imeDispatcher);
+                    + " imeDispatcher=" + imeDispatcher
+                    + " cs=" + cs);
         }
-
-        if (!mUserManagerInternal.isUserRunning(userId)) {
-            // There is a chance that we hit here because of race condition. Let's just
-            // return an error code instead of crashing the caller process, which at
-            // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an
-            // important process.
-            Slog.w(TAG, "User #" + userId + " is not running.");
-            return InputBindResult.INVALID_USER;
-        }
-
-        final ClientState cs = mClientController.getClient(client.asBinder());
-        if (cs == null) {
-            throw new IllegalArgumentException("Unknown client " + client.asBinder());
-        }
-
         final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
                 windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId);
         switch (imeClientFocus) {
@@ -3759,24 +3795,6 @@
                 return InputBindResult.INVALID_DISPLAY_ID;
         }
 
-        if (mUserSwitchHandlerTask != null) {
-            // There is already an on-going pending user switch task.
-            final int nextUserId = mUserSwitchHandlerTask.mToUserId;
-            if (userId == nextUserId) {
-                scheduleSwitchUserTaskLocked(userId, cs.mClient);
-                return InputBindResult.USER_SWITCHING;
-            }
-            final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds(
-                    mSettings.getUserId(), false /* enabledOnly */);
-            for (int profileId : profileIdsWithDisabled) {
-                if (profileId == userId) {
-                    scheduleSwitchUserTaskLocked(userId, cs.mClient);
-                    return InputBindResult.USER_SWITCHING;
-                }
-            }
-            return InputBindResult.INVALID_USER;
-        }
-
         final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
         // In case mShowForced flag affects the next client to keep IME visible, when the current
         // client is leaving due to the next focused client, we clear mShowForced flag when the
@@ -5366,7 +5384,11 @@
 
         if (!setSubtypeOnly) {
             // Set InputMethod here
-            mSettings.putSelectedInputMethod(imi != null ? imi.getId() : "");
+            final String imeId = imi != null ? imi.getId() : "";
+            mSettings.putSelectedInputMethod(imeId);
+            if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
+                mSettings.putSelectedDefaultDeviceInputMethod(imeId);
+            }
         }
     }
 
@@ -5509,6 +5531,9 @@
             return false; // IME is not found or not enabled.
         }
         settings.putSelectedInputMethod(imeId);
+        if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
+            settings.putSelectedDefaultDeviceInputMethod(imeId);
+        }
         settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
         return true;
     }
@@ -6555,6 +6580,9 @@
 
                         // Reset selected IME.
                         settings.putSelectedInputMethod(nextIme);
+                        if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
+                            settings.putSelectedDefaultDeviceInputMethod(nextIme);
+                        }
                         settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
                     }
                     out.println("Reset current and enabled IMEs for user #" + userId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
index a51002b..e444db1 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
@@ -36,7 +36,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -88,12 +87,6 @@
         mMethodMap = methodMap;
         mMethodList = methodMap.values();
         mUserId = userId;
-        String ime = getSelectedInputMethod();
-        String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
-        if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) {
-            putSelectedInputMethod(defaultDeviceIme);
-            putSelectedDefaultDeviceInputMethod(null);
-        }
     }
 
     @AnyThread
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4da2cc9..1c9bbab 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2049,7 +2049,9 @@
                 if (!mUserProfiles.isProfileUser(userId)) {
                     mConditionProviders.onUserUnlocked(userId);
                     mListeners.onUserUnlocked(userId);
-                    mZenModeHelper.onUserUnlocked(userId);
+                    if (!android.app.Flags.modesApi()) {
+                        mZenModeHelper.onUserUnlocked(userId);
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 41ff415..2f20bbe 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -328,6 +328,7 @@
         }
     }
 
+    // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
     public void onUserUnlocked(int user) {
         loadConfigForUser(user, "onUserUnlocked");
     }
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/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 86d05d9..25a39cc 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -257,7 +257,7 @@
     private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
             @NonNull AndroidPackage overlayPackage, int userId) {
         String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
-        if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
+        if (targetOverlayableName != null) {
             try {
                 OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
                         targetPackage.getPackageName(), targetOverlayableName, userId);
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index a61b03f..b9464d9 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -32,7 +32,6 @@
 import static android.os.Trace.TRACE_TAG_RRO;
 import static android.os.Trace.traceBegin;
 import static android.os.Trace.traceEnd;
-
 import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException;
 
 import android.annotation.NonNull;
@@ -363,7 +362,7 @@
                 defaultPackages.add(packageName);
             }
         }
-        return defaultPackages.toArray(new String[0]);
+        return defaultPackages.toArray(new String[defaultPackages.size()]);
     }
 
     private final class OverlayManagerPackageMonitor extends PackageMonitor {
@@ -1144,10 +1143,9 @@
     };
 
     private static final class PackageManagerHelperImpl implements PackageManagerHelper {
-        private static final class PackageStateUsers {
+        private static class PackageStateUsers {
             private PackageState mPackageState;
-            private Boolean mDefinesOverlayable = null;
-            private final ArraySet<Integer> mInstalledUsers = new ArraySet<>();
+            private final Set<Integer> mInstalledUsers = new ArraySet<>();
             private PackageStateUsers(@NonNull PackageState packageState) {
                 this.mPackageState = packageState;
             }
@@ -1162,7 +1160,7 @@
         // state may lead to contradictions within OMS. Better then to lag
         // behind until all pending intents have been processed.
         private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>();
-        private final ArraySet<Integer> mInitializedUsers = new ArraySet<>();
+        private final Set<Integer> mInitializedUsers = new ArraySet<>();
 
         PackageManagerHelperImpl(Context context) {
             mContext = context;
@@ -1178,7 +1176,8 @@
          */
         @NonNull
         public ArrayMap<String, PackageState> initializeForUser(final int userId) {
-            if (mInitializedUsers.add(userId)) {
+            if (!mInitializedUsers.contains(userId)) {
+                mInitializedUsers.add(userId);
                 mPackageManagerInternal.forEachPackageState((packageState -> {
                     if (packageState.getPkg() != null
                             && packageState.getUserStateOrDefault(userId).isInstalled()) {
@@ -1197,11 +1196,13 @@
             return userPackages;
         }
 
-        private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName,
+        @Override
+        @Nullable
+        public PackageState getPackageStateForUser(@NonNull final String packageName,
                 final int userId) {
             final PackageStateUsers pkg = mCache.get(packageName);
             if (pkg != null && pkg.mInstalledUsers.contains(userId)) {
-                return pkg;
+                return pkg.mPackageState;
             }
             try {
                 if (!mPackageManager.isPackageAvailable(packageName, userId)) {
@@ -1215,14 +1216,8 @@
             return addPackageUser(packageName, userId);
         }
 
-        @Override
-        public PackageState getPackageStateForUser(@NonNull final String packageName,
-                final int userId) {
-            final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId);
-            return pkg != null ? pkg.mPackageState : null;
-        }
-
-        private PackageStateUsers addPackageUser(@NonNull final String packageName,
+        @NonNull
+        private PackageState addPackageUser(@NonNull final String packageName,
                 final int user) {
             final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName);
             if (pkg == null) {
@@ -1234,20 +1229,20 @@
         }
 
         @NonNull
-        private PackageStateUsers addPackageUser(@NonNull final PackageState pkg,
+        private PackageState addPackageUser(@NonNull final PackageState pkg,
                 final int user) {
             PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName());
             if (pkgUsers == null) {
                 pkgUsers = new PackageStateUsers(pkg);
                 mCache.put(pkg.getPackageName(), pkgUsers);
-            } else if (pkgUsers.mPackageState != pkg) {
+            } else {
                 pkgUsers.mPackageState = pkg;
-                pkgUsers.mDefinesOverlayable = null;
             }
             pkgUsers.mInstalledUsers.add(user);
-            return pkgUsers;
+            return pkgUsers.mPackageState;
         }
 
+
         @NonNull
         private void removePackageUser(@NonNull final String packageName, final int user) {
             final PackageStateUsers pkgUsers = mCache.get(packageName);
@@ -1265,15 +1260,15 @@
             }
         }
 
+        @Nullable
         public PackageState onPackageAdded(@NonNull final String packageName, final int userId) {
-            final var pu = addPackageUser(packageName, userId);
-            return pu != null ? pu.mPackageState : null;
+            return addPackageUser(packageName, userId);
         }
 
+        @Nullable
         public PackageState onPackageUpdated(@NonNull final String packageName,
                 final int userId) {
-            final var pu = addPackageUser(packageName, userId);
-            return pu != null ? pu.mPackageState : null;
+            return addPackageUser(packageName, userId);
         }
 
         public void onPackageRemoved(@NonNull final String packageName, final int userId) {
@@ -1313,30 +1308,22 @@
             return (pkgs.length == 0) ? null : pkgs[0];
         }
 
+        @Nullable
         @Override
         public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
                 @NonNull String targetOverlayableName, int userId)
                 throws IOException {
-            final var psu = getRawPackageStateForUser(packageName, userId);
-            final var pkg = (psu == null || psu.mPackageState == null)
-                    ? null : psu.mPackageState.getAndroidPackage();
+            var packageState = getPackageStateForUser(packageName, userId);
+            var pkg = packageState == null ? null : packageState.getAndroidPackage();
             if (pkg == null) {
                 throw new IOException("Unable to get target package");
             }
 
-            if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) {
-                return null;
-            }
-
             ApkAssets apkAssets = null;
             try {
                 apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
                         ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
-                if (psu.mDefinesOverlayable == null) {
-                    psu.mDefinesOverlayable = apkAssets.definesOverlayable();
-                }
-                return Boolean.FALSE.equals(psu.mDefinesOverlayable)
-                        ? null : apkAssets.getOverlayableInfo(targetOverlayableName);
+                return apkAssets.getOverlayableInfo(targetOverlayableName);
             } finally {
                 if (apkAssets != null) {
                     try {
@@ -1350,29 +1337,24 @@
         @Override
         public boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
                 throws IOException {
-            final var psu = getRawPackageStateForUser(targetPackageName, userId);
-            var pkg = (psu == null || psu.mPackageState == null)
-                    ? null : psu.mPackageState.getAndroidPackage();
+            var packageState = getPackageStateForUser(targetPackageName, userId);
+            var pkg = packageState == null ? null : packageState.getAndroidPackage();
             if (pkg == null) {
                 throw new IOException("Unable to get target package");
             }
 
-            if (psu.mDefinesOverlayable == null) {
-                ApkAssets apkAssets = null;
-                try {
-                    apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
-                            ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
-                    psu.mDefinesOverlayable = apkAssets.definesOverlayable();
-                } finally {
-                    if (apkAssets != null) {
-                        try {
-                            apkAssets.close();
-                        } catch (Throwable ignored) {
-                        }
+            ApkAssets apkAssets = null;
+            try {
+                apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath());
+                return apkAssets.definesOverlayable();
+            } finally {
+                if (apkAssets != null) {
+                    try {
+                        apkAssets.close();
+                    } catch (Throwable ignored) {
                     }
                 }
             }
-            return psu.mDefinesOverlayable;
         }
 
         @Override
@@ -1563,7 +1545,8 @@
                 final OverlayPaths frameworkOverlays =
                         mImpl.getEnabledOverlayPaths("android", userId, false);
                 for (final String targetPackageName : targetPackageNames) {
-                    final var list = new OverlayPaths.Builder(frameworkOverlays);
+                    final OverlayPaths.Builder list = new OverlayPaths.Builder();
+                    list.addAll(frameworkOverlays);
                     if (!"android".equals(targetPackageName)) {
                         list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true));
                     }
@@ -1575,21 +1558,17 @@
             final HashSet<String> invalidPackages = new HashSet<>();
             pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages);
 
-            if (DEBUG || !invalidPackages.isEmpty()) {
-                for (final String targetPackageName : targetPackageNames) {
-                    if (DEBUG) {
-                        Slog.d(TAG,
-                                "-> Updating overlay: target=" + targetPackageName + " overlays=["
-                                        + pendingChanges.get(targetPackageName)
-                                        + "] userId=" + userId);
-                    }
+            for (final String targetPackageName : targetPackageNames) {
+                if (DEBUG) {
+                    Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=["
+                            + pendingChanges.get(targetPackageName)
+                            + "] userId=" + userId);
+                }
 
-                    if (invalidPackages.contains(targetPackageName)) {
-                        Slog.e(TAG, TextUtils.formatSimple(
-                                "Failed to change enabled overlays for %s user %d",
-                                targetPackageName,
-                                userId));
-                    }
+                if (invalidPackages.contains(targetPackageName)) {
+                    Slog.e(TAG, TextUtils.formatSimple(
+                            "Failed to change enabled overlays for %s user %d", targetPackageName,
+                            userId));
                 }
             }
             return new ArrayList<>(updatedPackages);
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index c1b6ccc..972c78d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -772,20 +772,24 @@
 
     OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName,
             final int userId, boolean includeImmutableOverlays) {
-        final var paths = new OverlayPaths.Builder();
-        mSettings.forEachMatching(userId, null, targetPackageName, oi -> {
+        final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName,
+                userId);
+        final OverlayPaths.Builder paths = new OverlayPaths.Builder();
+        final int n = overlays.size();
+        for (int i = 0; i < n; i++) {
+            final OverlayInfo oi = overlays.get(i);
             if (!oi.isEnabled()) {
-                return;
+                continue;
             }
             if (!includeImmutableOverlays && !oi.isMutable) {
-                return;
+                continue;
             }
             if (oi.isFabricated()) {
                 paths.addNonApkPath(oi.baseCodePath);
             } else {
                 paths.addApkPath(oi.baseCodePath);
             }
-        });
+        }
         return paths.build();
     }
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index b8b49f3e..eae614a 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -47,7 +47,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
@@ -183,23 +182,6 @@
         return CollectionUtils.map(items, SettingsItem::getOverlayInfo);
     }
 
-    void forEachMatching(int userId, String overlayName, String targetPackageName,
-            @NonNull Consumer<OverlayInfo> consumer) {
-        for (int i = 0, n = mItems.size(); i < n; i++) {
-            final SettingsItem item = mItems.get(i);
-            if (item.getUserId() != userId) {
-                continue;
-            }
-            if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) {
-                continue;
-            }
-            if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) {
-                continue;
-            }
-            consumer.accept(item.getOverlayInfo());
-        }
-    }
-
     ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
         final List<SettingsItem> items = selectWhereUser(userId);
 
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index bc0173a..43328fc 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -1726,7 +1726,8 @@
                                     .scheme("android-app")
                                     .authority(callingPackage)
                                     .build())
-                    .setPackage(installerPackageName);
+                    .setPackage(installerPackageName)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 474b590..339b1e7 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -19,7 +19,6 @@
 import static android.app.ActivityManager.START_ABORTED;
 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;
@@ -286,10 +285,16 @@
             Slog.e(TAG, TextUtils.formatSimple(
                     "Unexpected error occurred while unarchiving package %s: %s.", packageName,
                     t.getLocalizedMessage()));
-            return START_ABORTED;
         }
 
-        return START_SUCCESS;
+        // We return STATUS_ABORTED because:
+        // 1. Archived App is not actually present during activity start. Hence the unarchival
+        // start should be treated as an error code.
+        // 2. STATUS_ABORTED is not visible to the end consumers. Hence, it will not change user
+        // experience.
+        // 3. Returning STATUS_ABORTED helps us avoid manually handling of different cases like
+        // aborting activity options, animations etc in the Windows Manager.
+        return START_ABORTED;
     }
 
     /**
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/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 8549957..37f3825 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -347,7 +347,7 @@
         for (Rect crop : relativeCropHints) {
             Rect originalRect = new Rect(crop);
             originalRect.scale(wallpaper.mSampleSize);
-            originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right);
+            originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.top);
             result.add(originalRect);
         }
         return result;
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/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d6f52b8..85580ac 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1034,20 +1034,19 @@
         }
 
         if (err == ActivityManager.START_SUCCESS && aInfo == null) {
+            // We couldn't find the specific class specified in the Intent.
+            err = ActivityManager.START_CLASS_NOT_FOUND;
+
             if (isArchivingEnabled()) {
                 PackageArchiver packageArchiver = mService
                         .getPackageManagerInternalLocked()
                         .getPackageArchiver();
                 if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) {
-                    return packageArchiver
+                    err = packageArchiver
                             .requestUnarchiveOnActivityStart(
                                     intent, callingPackage, mRequest.userId, realCallingUid);
                 }
             }
-
-            // We couldn't find the specific class specified in the Intent.
-            // Also the end of the line.
-            err = ActivityManager.START_CLASS_NOT_FOUND;
         }
 
         if (err == ActivityManager.START_SUCCESS && sourceRecord != null
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 82dbf8d..716aee3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1429,14 +1429,6 @@
         return mTokenMap.get(binder);
     }
 
-    ActivityRecord getActivityRecord(IBinder binder) {
-        final WindowToken token = getWindowToken(binder);
-        if (token == null) {
-            return null;
-        }
-        return token.asActivityRecord();
-    }
-
     void addWindowToken(IBinder binder, WindowToken token) {
         final DisplayContent dc = mWmService.mRoot.getWindowTokenDisplay(token);
         if (dc != null) {
@@ -2250,7 +2242,6 @@
             }
         }
 
-        mWmService.mDisplayManagerInternal.performTraversal(transaction);
         if (shellTransitions) {
             // Before setDisplayProjection is applied by the start transaction of transition,
             // set the transform hint to avoid using surface in old rotation.
@@ -6985,7 +6976,7 @@
 
         @Override
         public void onAppTransitionFinishedLocked(IBinder token) {
-            final ActivityRecord r = getActivityRecord(token);
+            final ActivityRecord r = ActivityRecord.forTokenLocked(token);
             // Ignore the animating recents so the fixed rotation transform won't be switched twice
             // by finishing the recents animation and moving it to top. That also avoids flickering
             // due to wait for previous activity to be paused if it supports PiP that ignores the
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 587cc74..25646f1 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -249,6 +249,8 @@
     /** Reference to default display so we can quickly look it up. */
     private DisplayContent mDefaultDisplay;
     private final SparseArray<IntArray> mDisplayAccessUIDs = new SparseArray<>();
+    private final SparseArray<SurfaceControl.Transaction> mDisplayTransactions =
+            new SparseArray<>();
 
     /** The current user */
     int mCurrentUser;
@@ -569,22 +571,6 @@
         }, true /* traverseTopToBottom */);
     }
 
-    /**
-     * Returns the app window token for the input binder if it exist in the system.
-     * NOTE: Only one AppWindowToken is allowed to exist in the system for a binder token, since
-     * AppWindowToken represents an activity which can only exist on one display.
-     */
-    ActivityRecord getActivityRecord(IBinder binder) {
-        for (int i = mChildren.size() - 1; i >= 0; --i) {
-            final DisplayContent dc = mChildren.get(i);
-            final ActivityRecord activity = dc.getActivityRecord(binder);
-            if (activity != null) {
-                return activity;
-            }
-        }
-        return null;
-    }
-
     /** Returns the window token for the input binder if it exist in the system. */
     WindowToken getWindowToken(IBinder binder) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
@@ -991,11 +977,13 @@
         for (int j = 0; j < count; ++j) {
             final DisplayContent dc = mChildren.get(j);
             dc.applySurfaceChangesTransaction();
+            mDisplayTransactions.append(dc.mDisplayId, dc.getSyncTransaction());
         }
 
         // Give the display manager a chance to adjust properties like display rotation if it needs
         // to.
-        mWmService.mDisplayManagerInternal.performTraversal(t);
+        mWmService.mDisplayManagerInternal.performTraversal(t, mDisplayTransactions);
+        mDisplayTransactions.clear();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 8f9ed83..5b51776 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5070,7 +5070,7 @@
             mHandler.removeMessages(TRANSLUCENT_TIMEOUT_MSG);
 
             if (waitingActivity != null) {
-                mWmService.setWindowOpaqueLocked(waitingActivity.token, false);
+                waitingActivity.setMainWindowOpaque(false);
                 if (waitingActivity.attachedToProcess()) {
                     try {
                         waitingActivity.app.getThread().scheduleTranslucentConversionComplete(
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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 426694d..fc23700 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1073,7 +1073,7 @@
 
         @Override
         public void onAppTransitionFinishedLocked(IBinder token) {
-            final ActivityRecord atoken = mRoot.getActivityRecord(token);
+            final ActivityRecord atoken = ActivityRecord.forTokenLocked(token);
             if (atoken == null) {
                 return;
             }
@@ -3105,13 +3105,6 @@
         return mRecentsAnimationController != null && mRecentsAnimationController.isTargetApp(r);
     }
 
-    void setWindowOpaqueLocked(IBinder token, boolean isOpaque) {
-        final ActivityRecord wtoken = mRoot.getActivityRecord(token);
-        if (wtoken != null) {
-            wtoken.setMainWindowOpaque(isOpaque);
-        }
-    }
-
     boolean isValidPictureInPictureAspectRatio(DisplayContent displayContent, float aspectRatio) {
         return displayContent.getPinnedTaskController().isValidPictureInPictureAspectRatio(
                 aspectRatio);
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/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index 56423b9..897afbf 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -73,7 +73,6 @@
         "androidx.annotation_annotation",
         "androidx.test.rules",
         "framework",
-        "mockito_ravenwood",
         "ravenwood-runtime",
         "ravenwood-utils",
         "services",
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 75febd9..42bcb33 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -103,6 +103,7 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.SparseArray;
 import android.view.ContentRecordingSession;
 import android.view.Display;
 import android.view.DisplayAdjustments;
@@ -432,7 +433,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -507,7 +508,7 @@
             assertTrue(expectedDisplayTypeToViewPortTypeMapping.keySet().contains(info.type));
         }
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -559,7 +560,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -594,7 +595,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
@@ -632,7 +633,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
@@ -667,7 +668,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
@@ -947,7 +948,7 @@
                 PACKAGE_NAME);
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -1439,7 +1440,7 @@
                 PACKAGE_NAME);
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -1694,7 +1695,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
 
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
 
         // flush the handler
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
@@ -1728,7 +1729,7 @@
                 null /* projection */, PACKAGE_NAME);
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
         DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
         assertNotNull(ddi);
@@ -1797,7 +1798,7 @@
                 mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        performTraversalInternal(displayManager);
         displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
         DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
         assertNotNull(ddi);
@@ -2912,6 +2913,11 @@
         assertEquals(expectedMode, displayInfo.getMode());
     }
 
+    private void performTraversalInternal(DisplayManagerService displayManager) {
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class),
+                new SparseArray<>());
+    }
+
     private int getDisplayIdForDisplayDevice(
             DisplayManagerService displayManager,
             DisplayManagerService.BinderService displayManagerBinderService,
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index f49f638..64fef68 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -81,7 +81,6 @@
         "androidx.annotation_annotation",
         "androidx.test.rules",
         "truth",
-        "mockito_ravenwood",
     ],
     srcs: [
         ":power_stats_ravenwood_tests",
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 5611415..ad6e2c6 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -153,7 +153,6 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.test.rules",
-        "mockito_ravenwood",
         "services.core",
     ],
     srcs: [
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/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index 1c6d36b..ea84eb2 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -36,6 +36,7 @@
 import android.os.IDumpstateListener;
 import android.os.Process;
 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;
@@ -48,6 +49,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -118,6 +120,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
     public void testBugreportFileManagerFileExists() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         mBugreportFileManager.addBugreportFileForCaller(
@@ -137,6 +140,7 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
+    @Ignore
     public void testBugreportFileManagerKeepFilesOnRetrieval() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         mBugreportFileManager.addBugreportFileForCaller(
@@ -150,6 +154,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
     public void testBugreportFileManagerMultipleFiles() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         mBugreportFileManager.addBugreportFileForCaller(
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 839cf7c..c247c08 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -82,6 +82,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IpcDataCache;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
@@ -174,6 +175,11 @@
 
 
     @Before
+    public void disableProcessCaches() {
+        IpcDataCache.disableForTestMode();
+    }
+
+    @Before
     public void setUp() {
         // The AIDL stub will use PermissionEnforcer to check permission from the caller.
         mPermissionEnforcer = new FakePermissionEnforcer();
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 5d114f4..7462201 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -5202,7 +5202,7 @@
         // rules for a missing package, created a long time ago and deleted a long time ago
         config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo));
 
-        mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up.
+        mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
 
         assertThat(mZenModeHelper.mConfig.automaticRules.keySet())
                 .containsExactly("ar1", "ar2", "ar3", "ar4");
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/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index 09f677e7..46e14d51 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -16,8 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -38,13 +36,12 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -56,13 +53,8 @@
  */
 @SmallTest
 @Presubmit
-public class ClientLifecycleManagerTests {
-
-    @Rule(order = 0)
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
-    @Rule(order = 1)
-    public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule();
+@RunWith(WindowTestRunner.class)
+public class ClientLifecycleManagerTests extends SystemServiceTestsBase {
 
     @Mock
     private IBinder mClientBinder;
@@ -86,7 +78,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        mWms = mSystemServices.getWindowManagerService();
+        mWms = mSystemServicesTestRule.getWindowManagerService();
         mLifecycleManager = spy(new ClientLifecycleManager());
         mLifecycleManager.setWindowManager(mWms);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index be83744..0ee78e3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1802,8 +1802,7 @@
         @Override
         public void addStartingWindow(StartingWindowInfo info) {
             synchronized (mWMService.mGlobalLock) {
-                final ActivityRecord activity = mWMService.mRoot.getActivityRecord(
-                        info.appToken);
+                final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken);
                 IWindow iWindow = mock(IWindow.class);
                 doReturn(mock(IBinder.class)).when(iWindow).asBinder();
                 final WindowState window = WindowTestsBase.createWindow(null,
@@ -1825,8 +1824,7 @@
                 final IBinder appToken = mTaskAppMap.get(removalInfo.taskId);
                 if (appToken != null) {
                     mTaskAppMap.remove(removalInfo.taskId);
-                    final ActivityRecord activity = mWMService.mRoot.getActivityRecord(
-                            appToken);
+                    final ActivityRecord activity = ActivityRecord.forTokenLocked(appToken);
                     WindowState win = mAppWindowMap.remove(appToken);
                     activity.removeChild(win);
                     activity.mStartingWindow = null;
diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java
index 7ccc27e..0c324e6 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -649,12 +649,19 @@
     }
 
     /**
-     * @return Reason for denial if the registration state is {@link #REGISTRATION_STATE_DENIED}.
-     * Depending on {@code accessNetworkTechnology}, the values are defined in 3GPP TS 24.008
-     * 10.5.3.6 for UMTS, 3GPP TS 24.301 9.9.3.9 for LTE, and 3GPP2 A.S0001 6.2.2.44 for CDMA
-     * @hide
+     * Get the 3GPP/3GPP2 reason code indicating why registration failed.
+     *
+     * Returns the reason code for non-transient registration failures. Typically this method will
+     * only return the last reason code received during a network selection procedure. The reason
+     * code is system-specific; however, the reason codes for both 3GPP and 3GPP2 systems are
+     * largely equivalent across generations.
+     *
+     * @return registration reject cause if available, otherwise 0. Depending on
+     * {@link #getAccessNetworkTechnology}, the values are defined in 3GPP TS 24.008 10.5.3.6 for
+     * WCDMA/UMTS, 3GPP TS 24.301 9.9.3.9 for LTE/EPS, 3GPP 24.501 Annex A for NR/5GS, or 3GPP2
+     * A.S0001 6.2.2.44 for CDMA.
      */
-    @SystemApi
+    @FlaggedApi(Flags.FLAG_NETWORK_REGISTRATION_INFO_REJECT_CAUSE)
     public int getRejectCause() {
         return mRejectCause;
     }