Merge "[expressive design] Update Category layout." into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0f3b1c3..033da2d 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4944,10 +4944,14 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
             final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
             synchronized (mLock) {
                 String pkgList[] = null;
-                switch (intent.getAction()) {
+                switch (action) {
                     case Intent.ACTION_QUERY_PACKAGE_RESTART:
                         pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
                         for (String packageName : pkgList) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 2c6e1cb..3ffab90 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6854,6 +6854,47 @@
     method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri);
   }
 
+  @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style {
+    ctor public Notification.ProgressStyle();
+    method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment);
+    method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step);
+    method public int getProgress();
+    method @Nullable public android.graphics.drawable.Icon getProgressEndIcon();
+    method public int getProgressMax();
+    method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments();
+    method @Nullable public android.graphics.drawable.Icon getProgressStartIcon();
+    method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps();
+    method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon();
+    method public boolean isProgressIndeterminate();
+    method public boolean isStyledByProgress();
+    method @NonNull public android.app.Notification.ProgressStyle setProgress(int);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean);
+  }
+
+  public static final class Notification.ProgressStyle.Segment {
+    ctor public Notification.ProgressStyle.Segment(int);
+    method @ColorInt public int getColor();
+    method public int getLength();
+    method public int getStableId();
+    method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int);
+    method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int);
+  }
+
+  public static final class Notification.ProgressStyle.Step {
+    ctor public Notification.ProgressStyle.Step(int);
+    method @ColorInt public int getColor();
+    method public int getPosition();
+    method public int getStableId();
+    method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int);
+    method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int);
+  }
+
   public abstract static class Notification.Style {
     ctor @Deprecated public Notification.Style();
     method public android.app.Notification build();
@@ -7015,6 +7056,7 @@
     method public boolean areNotificationsEnabled();
     method public boolean areNotificationsPaused();
     method public boolean canNotifyAsPackage(@NonNull String);
+    method @FlaggedApi("android.app.api_rich_ongoing") public boolean canPostPromotedNotifications();
     method public boolean canUseFullScreenIntent();
     method public void cancel(int);
     method public void cancel(@Nullable String, int);
@@ -8731,13 +8773,15 @@
 package android.app.appfunctions {
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
-    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
   }
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
 
@@ -46654,6 +46698,8 @@
     field public static final int TYPE_IMS = 64; // 0x40
     field public static final int TYPE_MCX = 1024; // 0x400
     field public static final int TYPE_MMS = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PAID = 65536; // 0x10000
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PRIVATE = 131072; // 0x20000
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int TYPE_RCS = 32768; // 0x8000
     field public static final int TYPE_SUPL = 4; // 0x4
     field public static final int TYPE_VSIM = 4096; // 0x1000
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index cc0354c..bc34f5b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6822,6 +6822,27 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> CREATOR;
   }
 
+  @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioCapabilities();
+    method @NonNull public byte[] getData();
+    method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra> getKeyphrases();
+    method public boolean isAllowMultipleTriggers();
+    method public boolean isCaptureRequested();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
+  }
+
+  public static final class SoundTrigger.RecognitionConfig.Builder {
+    ctor public SoundTrigger.RecognitionConfig.Builder();
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig build();
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAllowMultipleTriggers(boolean);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAudioCapabilities(int);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setCaptureRequested(boolean);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setData(@Nullable byte[]);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setKeyphrases(@NonNull java.util.Collection<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
+  }
+
   public static class SoundTrigger.RecognitionEvent {
     method @Nullable public android.media.AudioFormat getCaptureFormat();
     method public int getCaptureSession();
@@ -7772,10 +7793,16 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
     method public int getDetectionServiceOperationsTimeout();
     method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties();
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
   }
 
@@ -13020,6 +13047,8 @@
     method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
     method public final void unsnoozeNotification(@NonNull String);
     field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+    field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+    field @FlaggedApi("android.service.notification.notification_classification") public static final String EXTRA_NOTIFICATION_KEY = "android.service.notification.extra.NOTIFICATION_KEY";
     field public static final String FEEDBACK_RATING = "feedback.rating";
     field public static final String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
     field public static final int SOURCE_FROM_APP = 0; // 0x0
@@ -15859,6 +15888,8 @@
     field public static final String TYPE_IMS_STRING = "ims";
     field public static final String TYPE_MCX_STRING = "mcx";
     field public static final String TYPE_MMS_STRING = "mms";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String TYPE_RCS_STRING = "rcs";
     field public static final String TYPE_SUPL_STRING = "supl";
     field public static final String TYPE_VSIM_STRING = "vsim";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 72a68f8..0a10920 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -398,6 +398,7 @@
     method public android.content.ComponentName getEffectsSuppressor();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
     method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
+    method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
     method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
@@ -1613,15 +1614,15 @@
   public final class CameraManager {
     method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
     method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
-    method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
+    method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
     method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String);
     field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait";
     field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
   }
 
   public abstract static class CameraManager.AvailabilityCallback {
@@ -1885,17 +1886,9 @@
     ctor public SoundTrigger.ModuleProperties(int, @NonNull String, @NonNull String, @NonNull String, int, @NonNull String, int, int, int, int, boolean, int, boolean, int, boolean, int);
   }
 
-  public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
-    ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
+  @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+    ctor @Deprecated public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
     ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]);
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
-    field public final boolean allowMultipleTriggers;
-    field public final int audioCapabilities;
-    field public final boolean captureRequested;
-    field @NonNull public final byte[] data;
-    field @NonNull public final android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[] keyphrases;
   }
 
   public static class SoundTrigger.RecognitionEvent {
@@ -2216,7 +2209,7 @@
   public class SoundTriggerInstrumentation.RecognitionSession {
     method public void clearRecognitionCallback();
     method public int getAudioSession();
-    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
     method public void setRecognitionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionCallback);
     method public void triggerAbortRecognition();
     method public void triggerRecognitionEvent(@NonNull byte[], @Nullable java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
@@ -2227,7 +2220,6 @@
     method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForModule(@NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties);
     method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForTestModule();
     method @NonNull public static java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties();
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
   }
 
   public static class SoundTriggerManager.Model {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 92bca3c..9904632 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -21,14 +21,52 @@
         "**/*.aidl",
         ":framework-nfc-non-updatable-sources",
         ":messagequeue-gen",
+        ":ranging_stack_mock_initializer",
     ],
     // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
     exclude_srcs: [
         "android/os/*MessageQueue/**/*.java",
+        "android/ranging/**/*.java",
     ],
     visibility: ["//frameworks/base"],
 }
 
+//Mock to allow service registry for ranging stack.
+//TODO(b/331206299): Remove this after RELEASE_RANGING_STACK is ramped up to next.
+soong_config_module_type {
+    name: "ranging_stack_framework_mock_init",
+    module_type: "genrule",
+    config_namespace: "bootclasspath",
+    bool_variables: [
+        "release_ranging_stack",
+    ],
+    properties: [
+        "srcs",
+        "cmd",
+        "out",
+    ],
+}
+
+// The actual RangingFrameworkInitializer is present in packages/modules/Uwb/ranging/framework.
+// Mock RangingFrameworkInitializer does nothing and allows to successfully build
+// SystemServiceRegistry after registering for system service in SystemServiceRegistry both with
+// and without build flag RELEASE_RANGING_STACK enabled.
+ranging_stack_framework_mock_init {
+    name: "ranging_stack_mock_initializer",
+    soong_config_variables: {
+        release_ranging_stack: {
+            cmd: "touch $(out)",
+            // Adding an empty file as out is mandatory.
+            out: ["android/ranging/empty_ranging_fw.txt"],
+            conditions_default: {
+                srcs: ["android/ranging/mock/RangingFrameworkInitializer.java"],
+                cmd: "mkdir -p android/ranging/; cp $(in) $(out);",
+                out: ["android/ranging/RangingFrameworkInitializer.java"],
+            },
+        },
+    },
+}
+
 // Add selected MessageQueue.java implementation to srcs
 soong_config_module_type {
     name: "release_package_messagequeue_implementation_srcs",
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3bc3a93..5b556cc 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -230,6 +230,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.content.ReferrerIntent;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.DebugStore;
@@ -1301,6 +1302,7 @@
                 long[] disabledCompatChanges,
                 long[] loggableCompatChanges,
                 SharedMemory serializedSystemFontMap,
+                FileDescriptor applicationSharedMemoryFd,
                 long startRequestedElapsedTime,
                 long startRequestedUptime) {
             if (services != null) {
@@ -1329,6 +1331,16 @@
                 ServiceManager.initServiceCache(services);
             }
 
+            // This must be initialized as early as possible to ensure availability for any
+            // downstream callers.
+            if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()) {
+                ApplicationSharedMemory instance =
+                        ApplicationSharedMemory.fromFileDescriptor(
+                                applicationSharedMemoryFd, /* mutable= */ false);
+                instance.closeFileDescriptor();
+                ApplicationSharedMemory.setInstance(instance);
+            }
+
             setCoreSettings(coreSettings);
 
             AppBindData data = new AppBindData();
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index fad289c..edcdb6c 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -1047,7 +1047,7 @@
     private static String startComponentToString(@StartComponent int startComponent) {
         return switch (startComponent) {
             case START_COMPONENT_ACTIVITY -> "ACTIVITY";
-            case START_COMPONENT_BROADCAST -> "SERVICE";
+            case START_COMPONENT_BROADCAST -> "BROADCAST";
             case START_COMPONENT_CONTENT_PROVIDER -> "CONTENT PROVIDER";
             case START_COMPONENT_SERVICE -> "SERVICE";
             case START_COMPONENT_OTHER -> "OTHER";
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 9f3829e..06d01ec 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -92,6 +92,7 @@
             in Bundle coreSettings, in String buildSerial, in AutofillOptions autofillOptions,
             in ContentCaptureOptions contentCaptureOptions, in long[] disabledCompatChanges,
             in long[] loggableCompatChanges, in SharedMemory serializedSystemFontMap,
+            in FileDescriptor applicationSharedMemoryFd,
             long startRequestedElapsedTime, long startRequestedUptime);
     void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
     void scheduleExit();
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index b9fe356..8a54b5d 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -258,6 +258,7 @@
     @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"})
     void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener);
 
-    void setCanBePromoted(String pkg, int uid, boolean promote);
-    boolean canBePromoted(String pkg, int uid);
+    void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser);
+    boolean appCanBePromoted(String pkg, int uid);
+    boolean canBePromoted(String pkg);
 }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 392a1f1..a39f216 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -121,7 +121,6 @@
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -783,10 +782,32 @@
     @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
     public static final int FLAG_PROMOTED_ONGOING = 0x00040000;
 
-    private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
-            BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
-            DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
-            MessagingStyle.class, CallStyle.class);
+    private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of(
+            BigTextStyle.class,
+            BigPictureStyle.class,
+            InboxStyle.class,
+            MediaStyle.class,
+            DecoratedCustomViewStyle.class,
+            DecoratedMediaCustomViewStyle.class,
+            MessagingStyle.class,
+            CallStyle.class
+    );
+
+    private static boolean isPlatformStyle(Style style) {
+        if (style == null) {
+            return false;
+        }
+
+        if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) {
+            return true;
+        }
+
+        if (Flags.apiRichOngoing()) {
+            return style.getClass() == ProgressStyle.class;
+        }
+
+        return false;
+    }
 
     /** @hide */
     @IntDef(flag = true, prefix = {"FLAG_"}, value = {
@@ -1620,6 +1641,66 @@
     public static final String EXTRA_COLORIZED = "android.colorized";
 
     /**
+     * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment}
+     * bundles provided by a
+     * {@link android.app.Notification.ProgressStyle} notification as supplied to
+     * {@link ProgressStyle#setProgressSegments}
+     * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}.
+     * This extra is a parcelable array list of bundles.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments";
+
+    /**
+     * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step}
+     * bundles provided by a
+     * {@link android.app.Notification.ProgressStyle} notification as supplied to
+     * {@link ProgressStyle#setProgressSteps}
+     * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}.
+     * This extra is a parcelable array list of bundles.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps";
+
+    /**
+     * {@link #extras} key: whether the progress bar should be styled by its progress as
+     * supplied to {@link ProgressStyle#setStyledByProgress}.
+     * This extra is a boolean.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressTrackerIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressStartIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown at the end of the progress bar in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressEndIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon";
+
+    /**
      * @hide
      */
     public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo";
@@ -3072,6 +3153,9 @@
 
         if (Flags.apiRichOngoing()) {
             visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class));
         }
 
         if (mBubbleMetadata != null) {
@@ -6630,7 +6714,7 @@
             // Custom views which come from a platform style class are safe, and thus do not need to
             // be wrapped.  Any subclass of those styles has the opportunity to make arbitrary
             // changes to the RemoteViews, and thus can't be trusted as a fully vetted view.
-            if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) {
+            if (fromStyle && isPlatformStyle(mStyle)) {
                 return false;
             }
             return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S;
@@ -7971,6 +8055,12 @@
                 return innerClass;
             }
         }
+
+        if (Flags.apiRichOngoing()) {
+            if (templateClass.equals(ProgressStyle.class.getName())) {
+                return ProgressStyle.class;
+            }
+        }
         return null;
     }
 
@@ -11220,6 +11310,724 @@
         }
     }
 
+
+    /**
+     * A Notification Style used to to define a notification whose expanded state includes
+     * a highly customizable progress bar with segments, steps, a custom tracker icon,
+     * and custom icons at the start and end of the progress bar.
+     *
+     * This style is suggested for use cases where the app is showing a tracker to the
+     * user of a thing they are interested in: the location of a car on its way
+     * to pick them up, food being delivered, or their own progress in a navigation
+     * journey.
+     *
+     * To use this style with your Notification, feed it to
+     * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+     * <pre class="prettyprint">
+     * new Notification.Builder(context)
+     *   .setSmallIcon(R.drawable.ic_notification)
+     *   .setColor(Color.GREEN)
+     *   .setColorized(true)
+     *   .setContentTitle("Arrive 10:08 AM").
+     *   .setContentText("Dominique Ansel Bakery Soho")
+     *   .addAction(new Notification.Action("Exit navigation",...))
+     *   .setStyle(new Notification.ProgressStyle()
+     *       .setStyledByProgress(false)
+     *       .setProgress(456)
+     *       .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker))
+     *       .addProgressSegment(new Segment(41).setColor(Color.BLACK))
+     *       .addProgressSegment(new Segment(552).setColor(Color.YELLOW))
+     *       .addProgressSegment(new Segment(253).setColor(Color.YELLOW))
+     *       .addProgressSegment(new Segment(94).setColor(Color.BLUE))
+     *       .addProgressStep(new Step(60).setColor(Color.RED))
+     *       .addProgressStep(new Step(560).setColor(Color.YELLOW))
+     *   )
+     * </pre>
+     *
+     *
+     *
+     * NOTE: The progress bar layout will be mirrored for RTL layout.
+     * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by
+     * the values set on this style object when the notification is built.
+     *
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static class ProgressStyle extends Notification.Style {
+        private static final String KEY_ELEMENT_STABLE_ID = "stableId";
+        private static final String KEY_ELEMENT_COLOR = "colorInt";
+        private static final String KEY_SEGMENT_LENGTH = "length";
+        private static final String KEY_STEP_POSITION = "position";
+
+        private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15;
+        private static final int MAX_PROGRESS_STEP_LIMIT = 5;
+        private static final int DEFAULT_PROGRESS_MAX = 100;
+
+        private List<Segment> mProgressSegments = new ArrayList<>();
+        private List<Step> mProgressSteps = new ArrayList<>();
+
+        private int mProgress = 0;
+
+        private boolean mIndeterminate;
+
+        private boolean mIsStyledByProgress = true;
+
+        @Nullable
+        private Icon mTrackerIcon;
+        @Nullable
+        private Icon mStartIcon;
+        @Nullable
+        private Icon mEndIcon;
+
+        /**
+         * @hide
+         */
+        @Override
+        public boolean areNotificationsVisiblyDifferent(Style other) {
+            if (other == null || getClass() != other.getClass()) {
+                return true;
+            }
+
+            final ProgressStyle progressStyle = (ProgressStyle) other;
+
+            /**
+             * @see #setProgressIndeterminate
+             */
+            if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) {
+                return true;
+            }
+            boolean nonIndeterminateCheckResult = false;
+            if (!mIndeterminate) {
+                nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress)
+                        || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress)
+                        || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments)
+                        || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps)
+                        || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon);
+            }
+
+            return !Objects.equals(mStartIcon, progressStyle.mStartIcon)
+                    || !Objects.equals(mEndIcon, progressStyle.mEndIcon)
+                    || nonIndeterminateCheckResult;
+        }
+
+        /**
+         * Gets the segments that define the background layer of the progress bar.
+         *
+         * If no segments are provided, the progress bar will be rendered with a single segment
+         * with length 100 and default color.
+         *
+         * @see #setProgressSegments
+         * @see #addProgressSegment
+         * @see Segment
+         */
+        public @NonNull List<Segment> getProgressSegments() {
+            return mProgressSegments;
+        }
+
+        /**
+         * Sets or replaces the segments of the progress bar.
+         *
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         * @see Segment
+         */
+        public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) {
+            mProgressSegments = new ArrayList<>(progressSegments.size());
+            return this;
+        }
+
+        /**
+         * Appends a segment to the end of the progress bar.
+         *
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         * @see Segment
+         */
+        public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) {
+            if (mProgressSegments == null) {
+                mProgressSegments = new ArrayList<>();
+            }
+            mProgressSegments.add(segment);
+
+            return this;
+        }
+
+        /**
+         * Gets the steps that are displayed on the progress bar.
+         *.
+         * @see #setProgressSteps
+         * @see #addProgressStep
+         * @see Step
+         */
+        public @NonNull List<Step> getProgressSteps() {
+            return mProgressSteps;
+        }
+
+        /**
+         * Replaces all the progress steps.
+         *
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         * @see Step
+         */
+        public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) {
+            mProgressSteps = new ArrayList<>(steps);
+            return this;
+        }
+
+        /**
+         * Adds another step.
+         *
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         *
+         * Steps can be added in any order, as their
+         * position within the progress bar is determined by their individual
+         * {@link Step#getPosition()}.
+         * @see Step
+         */
+        public @NonNull ProgressStyle addProgressStep(@NonNull Step step) {
+            if (mProgressSteps == null) {
+                mProgressSteps = new ArrayList<>();
+            }
+            mProgressSteps.add(step);
+
+            return this;
+        }
+
+        /**
+         * Gets the progress value of the progress bar.
+         * @see #setProgress
+         */
+        public int getProgress() {
+            return mProgress;
+        }
+
+        /**
+        * Specifies the progress (in the same units as {@link Segment#getLength()})
+        * of the tracker along the length of the bar.
+        *
+        * The max progress value is the sum of all Segment lengths.
+        * The default value is 0.
+        */
+        public @NonNull ProgressStyle setProgress(int progress) {
+            mProgress = progress;
+            return this;
+        }
+
+        /**
+         * Gets the sum of the lengths of all Segments in the style, which
+         * defines the maximum progress. Defaults to 100 when segments are omitted.
+         */
+        public int getProgressMax() {
+            final List<Segment> progressSegment = mProgressSegments;
+            if (progressSegment == null || progressSegment.isEmpty()) {
+                return DEFAULT_PROGRESS_MAX;
+            } else {
+                int progressMax = 0;
+                int validSegmentCount = 0;
+                for (int i = 0; i < progressSegment.size()
+                        && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) {
+                    int segmentLength = progressSegment.get(i).getLength();
+                    if (segmentLength > 0) {
+                        try {
+                            progressMax = Math.addExact(progressMax, segmentLength);
+                            validSegmentCount++;
+                        } catch (ArithmeticException e) {
+                            Log.e(TAG,
+                                    "Notification.ProgressStyle segment total overflowed.", e);
+                            return DEFAULT_PROGRESS_MAX;
+                        }
+                    }
+                }
+
+                if (validSegmentCount == 0) {
+                    return DEFAULT_PROGRESS_MAX;
+                }
+
+                return progressMax;
+            }
+
+        }
+
+        /**
+         * Get indeterminate value of the progress bar.
+         * @see #setProgressIndeterminate
+         */
+        public boolean isProgressIndeterminate() {
+            return mIndeterminate;
+        }
+
+        /**
+         * Used to indicate an initialization state without a known progress amount.
+         * When specified, the following fields are ignored:
+         * @see #setProgress
+         * @see #setProgressSegments
+         * @see #setProgressSteps
+         * @see #setProgressTrackerIcon
+         * @see #setStyledByProgress
+         *
+         * If the app provides exactly one Segment, that segment's color will be
+         * used to style the indeterminate bar.
+         */
+        public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) {
+            mIndeterminate = indeterminate;
+            return this;
+        }
+
+        /**
+         * Gets whether the progress bar's style is based on its progress.
+         * @see #setStyledByProgress
+         */
+        public boolean isStyledByProgress() {
+            return mIsStyledByProgress;
+        }
+
+        /**
+         * Indicates whether the segments and steps will be styled differently
+         * based on whether they are behind or ahead of the current progress.
+         * When true, segments appearing ahead of the current progress will be given a
+         * slightly different appearance to indicate that it is part of the progress bar
+         * that is not "filled".
+         * When false, all segments will be given the filled appearance, and it will be
+         * the app's responsibility to use #setProgressTrackerIcon or segment colors
+         * to make the current progress clear to the user.
+         * the default value is true.
+         */
+        public @NonNull ProgressStyle setStyledByProgress(boolean enabled) {
+            mIsStyledByProgress = enabled;
+            return this;
+        }
+
+
+        /**
+         * Gets the progress tracker icon for the progress bar.
+         * @see #setProgressTrackerIcon
+         */
+        public @Nullable Icon getProgressTrackerIcon() {
+            return mTrackerIcon;
+        }
+
+        /**
+         * An optional icon that can appear as an overlay on the bar at the point of
+         * current progress.
+         * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that
+         * aspect ratio range will be cropped.
+         * This icon will be mirrored in RTL.
+         */
+        public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) {
+            mTrackerIcon = trackerIcon;
+            return this;
+        }
+
+        /**
+         * Gets the progress bar start icon.
+         * @see #setProgressStartIcon
+         */
+        public @Nullable Icon getProgressStartIcon() {
+            return mStartIcon;
+        }
+
+        /**
+         * An optional square icon that appears at the start of the progress bar.
+         * This icon will be cropped to its central square.
+         * This icon will NOT be mirrored in RTL layouts.
+         */
+        public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) {
+            mStartIcon = startIcon;
+            return this;
+        }
+
+        /**
+         * Gets the progress bar end icon.
+         * @see #setProgressEndIcon(Icon)
+         */
+        public @Nullable Icon getProgressEndIcon() {
+            return mEndIcon;
+        }
+
+        /**
+         * An optional square icon that appears at the end of the progress bar.
+         * This icon will be cropped to its central square.
+         * This icon will NOT be mirrored in RTL layouts.
+         */
+        public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) {
+            mEndIcon = endIcon;
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void purgeResources() {
+            super.purgeResources();
+            if (mTrackerIcon != null) {
+                mTrackerIcon.convertToAshmem();
+            }
+            if (mStartIcon != null) {
+                mStartIcon.convertToAshmem();
+            }
+            if (mEndIcon != null) {
+                mEndIcon.convertToAshmem();
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void reduceImageSizes(Context context) {
+            super.reduceImageSizes(context);
+
+            final Resources resources = context.getResources();
+
+            int progressIconSize =
+                    resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size);
+            if (mStartIcon != null) {
+                mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+            }
+            if (mEndIcon != null) {
+                mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+            }
+            if (mTrackerIcon != null) {
+                int progressTrackerWidth = resources.getDimensionPixelSize(
+                        R.dimen.notification_progress_tracker_width);
+                int progressTrackerHeight = resources.getDimensionPixelSize(
+                        R.dimen.notification_progress_tracker_height);
+                mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void addExtras(Bundle extras) {
+            super.addExtras(extras);
+            extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS,
+                    getProgressSegmentsAsBundleList(mProgressSegments));
+            extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS,
+                    getProgressStepsAsBundleList(mProgressSteps));
+
+            extras.putInt(EXTRA_PROGRESS, mProgress);
+            extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate);
+            extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax());
+            extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress);
+
+            if (mTrackerIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_TRACKER_ICON);
+            }
+
+            if (mStartIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_START_ICON);
+            }
+
+            if (mEndIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_END_ICON);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        protected void restoreFromExtras(Bundle extras) {
+            super.restoreFromExtras(extras);
+            mProgressSegments = getProgressSegmentsFromBundleList(
+                    extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class));
+            mProgress = extras.getInt(EXTRA_PROGRESS, 0);
+            mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false);
+            mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true);
+            mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class);
+            mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class);
+            mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class);
+            mProgressSteps = getProgressStepsFromBundleList(
+                    extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class));
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public boolean displayCustomViewInline() {
+            // This is a lie; True is returned for progress notifications to make sure
+            // that the custom view is not used instead of the template, but it will not
+            // actually be included.
+            return true;
+        }
+
+        private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
+                @Nullable List<Segment> progressSegments) {
+            final ArrayList<Bundle> segments = new ArrayList<>();
+            if (progressSegments != null && !progressSegments.isEmpty()) {
+                for (int i = 0; i < progressSegments.size(); i++) {
+                    final Segment segment = progressSegments.get(i);
+                    if (segment.getLength() <= 0) {
+                        continue;
+                    }
+
+                    final Bundle bundle = new Bundle();
+                    bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength());
+                    bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId());
+                    bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor());
+
+                    segments.add(bundle);
+                }
+            }
+
+            return segments;
+        }
+
+        private static @NonNull List<Segment> getProgressSegmentsFromBundleList(
+                @Nullable List<Bundle> segmentBundleList) {
+            final ArrayList<Segment> segments = new ArrayList<>();
+            if (segmentBundleList != null && !segmentBundleList.isEmpty()) {
+                for (int i = 0; i < segmentBundleList.size(); i++) {
+                    final Bundle segmentBundle = segmentBundleList.get(i);
+                    final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH);
+                    if (length <= 0) {
+                        continue;
+                    }
+
+                    final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+                    final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+                            Notification.COLOR_DEFAULT);
+                    final Segment segment = new Segment(length)
+                            .setStableId(stableId).setColor(color);
+
+                    segments.add(segment);
+                }
+            }
+
+            return segments;
+        }
+
+        private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList(
+                @Nullable List<Step> progressSteps) {
+            final ArrayList<Bundle> steps = new ArrayList<>();
+            if (progressSteps != null && !progressSteps.isEmpty()) {
+                for (int i = 0; i < progressSteps.size(); i++) {
+                    final Step step = progressSteps.get(i);
+                    if (step.getPosition() < 0) {
+                        continue;
+                    }
+
+                    final Bundle bundle = new Bundle();
+                    bundle.putInt(KEY_STEP_POSITION, step.getPosition());
+                    bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId());
+                    bundle.putInt(KEY_ELEMENT_COLOR, step.getColor());
+
+                    steps.add(bundle);
+                }
+            }
+
+            return steps;
+        }
+
+        private static @NonNull List<Step> getProgressStepsFromBundleList(
+                @Nullable List<Bundle> stepBundleList) {
+            final ArrayList<Step> steps = new ArrayList<>();
+
+            if (stepBundleList != null && !stepBundleList.isEmpty()) {
+                for (int i = 0; i < stepBundleList.size(); i++) {
+                    final Bundle segmentBundle = stepBundleList.get(i);
+                    final int position = segmentBundle.getInt(KEY_STEP_POSITION);
+                    if (position < 0) {
+                        continue;
+                    }
+                    final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+                    final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+                            Notification.COLOR_DEFAULT);
+                    final Step step = new Step(position).setStableId(stableId).setColor(color);
+                    steps.add(step);
+                }
+            }
+
+            return steps;
+        }
+
+        /**
+         * A segment of the progress bar, which defines its length and color.
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         */
+        public static final class Segment {
+            private int mLength;
+            private int mStableId = 0;
+            @ColorInt
+            private int mColor = Notification.COLOR_DEFAULT;
+
+            /**
+             * Create a segment with a non-zero length.
+             * @param length
+             * See {@link #getLength}
+             */
+            public Segment(int length) {
+                mLength = length;
+            }
+
+            /**
+             * The length of this Segment within the progress bar.
+             * This value has no units, it is just relative to the length of other segments,
+             * and the value provided to {@link ProgressStyle#setProgress}.
+             */
+            public int getLength() {
+                return mLength;
+            }
+
+            /**
+             * Gets the stable id of this Segment.
+             *
+             * @see #setStableId
+             */
+            public int getStableId() {
+                return mStableId;
+            }
+
+            /**
+             * Optional ID used to uniquely identify the element across updates.
+             */
+            public @NonNull Segment setStableId(int stableId) {
+                mStableId = stableId;
+                return this;
+            }
+
+            /**
+             * Returns the color of this Segment.
+             *
+             * @see #setColor
+             */
+            @ColorInt
+            public int getColor() {
+                return mColor;
+            }
+
+            /**
+             * Optional color of this Segment
+             */
+            public @NonNull Segment setColor(@ColorInt int color) {
+                mColor = color;
+                return this;
+            }
+
+            /**
+             * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+             */
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                Segment segment = (Segment) o;
+                return mLength == segment.mLength && mStableId == segment.mStableId
+                        && mColor == segment.mColor;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mLength, mStableId, mColor);
+            }
+        }
+
+        /**
+         * A step within the progress bar, defining its position and color.
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         */
+        public static final class Step {
+
+            private int mPosition;
+            private int mStableId;
+            @ColorInt
+            private int mColor = Notification.COLOR_DEFAULT;
+
+            /**
+             * Create a step element.
+             * The position of this step on the progress bar
+             * relative to {@link ProgressStyle#getProgressMax}
+             * @param position
+             * See {@link #getPosition}
+             */
+            public Step(int position) {
+                mPosition = position;
+            }
+
+            /**
+             * Gets the position of this Step.
+             * The position of this step on the progress bar
+             * relative to {@link ProgressStyle#getProgressMax}.
+             */
+            public int getPosition() {
+                return mPosition;
+            }
+
+
+            /**
+             * Optional ID used to uniqurely identify the element across updates.
+             */
+            public int getStableId() {
+                return mStableId;
+            }
+
+            /**
+             * Optional ID used to uniqurely identify the element across updates.
+             */
+            public @NonNull Step setStableId(int stableId) {
+                mStableId = stableId;
+                return this;
+            }
+
+            /**
+             * Returns the color of this Segment.
+             *
+             * @see #setColor
+             */
+            @ColorInt
+            public int getColor() {
+                return mColor;
+            }
+
+            /**
+             * Optional color of this Segment
+             */
+            public @NonNull Step setColor(@ColorInt int color) {
+                mColor = color;
+                return this;
+            }
+
+            /**
+             * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+             */
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                Step step = (Step) o;
+                return mPosition == step.mPosition && mStableId == step.mStableId
+                        && mColor == step.mColor;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mPosition, mStableId, mColor);
+            }
+        }
+    }
+
     /**
      * Notification style for custom views that are decorated by the system
      *
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 83f9ff7..c7b84ae 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -953,6 +953,36 @@
     }
 
     /**
+     * Returns whether the calling app's properly formatted notifications can appear in a promoted
+     * format, which may result in higher ranking, appearances on additional surfaces, and richer
+     * presentation.
+     */
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+    public boolean canPostPromotedNotifications() {
+        INotificationManager service = getService();
+        try {
+            return service.canBePromoted(mContext.getPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Setter for {@link #canPostPromotedNotifications()}. Only callable by the OS.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+    public void setCanPostPromotedNotifications(@NonNull String pkg, int uid, boolean allowed) {
+        INotificationManager service = getService();
+        try {
+            service.setCanBePromoted(pkg, uid, allowed, true);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Creates a group container for {@link NotificationChannel} objects.
      *
      * This can be used to rename an existing group.
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 0e761fc..c17da24 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -54,198 +54,8 @@
  * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
  * but doesn't hold a lock across data fetches on query misses.
  *
- * The intended use case is caching frequently-read, seldom-changed information normally
- * retrieved across interprocess communication. Imagine that you've written a user birthday
- * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface
- * over binder. That binder interface looks something like this:
- *
- * <pre>
- * parcelable Birthday {
- *   int month;
- *   int day;
- * }
- * interface IUserBirthdayService {
- *   Birthday getUserBirthday(int userId);
- * }
- * </pre>
- *
- * Suppose the service implementation itself looks like this...
- *
- * <pre>
- * public class UserBirthdayServiceImpl implements IUserBirthdayService {
- *   private final HashMap&lt;Integer, Birthday%&gt; mUidToBirthday;
- *   {@literal @}Override
- *   public synchronized Birthday getUserBirthday(int userId) {
- *     return mUidToBirthday.get(userId);
- *   }
- *   private synchronized void updateBirthdays(Map&lt;Integer, Birthday%&gt; uidToBirthday) {
- *     mUidToBirthday.clear();
- *     mUidToBirthday.putAll(uidToBirthday);
- *   }
- * }
- * </pre>
- *
- * ... and we have a client in frameworks (loaded into every app process) that looks
- * like this:
- *
- * <pre>
- * public class ActivityThread {
- *   ...
- *   public Birthday getUserBirthday(int userId) {
- *     return GetService("birthdayd").getUserBirthday(userId);
- *   }
- *   ...
- * }
- * </pre>
- *
- * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call
- * to the birthdayd process and consult its database of birthdays. If we query user birthdays
- * frequently, we do a lot of work that we don't have to do, since user birthdays
- * change infrequently.
- *
- * PropertyInvalidatedCache is part of a pattern for optimizing this kind of
- * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client
- * this way:
- *
- * <pre>
- * public class ActivityThread {
- *   ...
- *   private final PropertyInvalidatedCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
- *       new PropertyInvalidatedCache.QueryHandler&lt;Integer, Birthday&gt;() {
- *           {@literal @}Override
- *           public Birthday apply(Integer) {
- *              return GetService("birthdayd").getUserBirthday(userId);
- *           }
- *       };
- *   private static final int BDAY_CACHE_MAX = 8;  // Maximum birthdays to cache
- *   private static final String BDAY_CACHE_KEY = "cache_key.birthdayd";
- *   private final PropertyInvalidatedCache&lt;Integer, Birthday%&gt; mBirthdayCache = new
- *     PropertyInvalidatedCache&lt;Integer, Birthday%&gt;(
- *             BDAY_CACHE_MAX, MODULE_SYSTEM, "getUserBirthday", mBirthdayQuery);
- *
- *   public void disableUserBirthdayCache() {
- *     mBirthdayCache.disableForCurrentProcess();
- *   }
- *   public void invalidateUserBirthdayCache() {
- *     mBirthdayCache.invalidateCache();
- *   }
- *   public Birthday getUserBirthday(int userId) {
- *     return mBirthdayCache.query(userId);
- *   }
- *   ...
- * }
- * </pre>
- *
- * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
- * for the first time; on subsequent queries, we return the already-known Birthday object.
- *
- * The second parameter to the IpcDataCache constructor is a string that identifies the "module"
- * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
- * string is permitted.  The third parameters is the name of the API being cached; this, too, can
- * any value.  The fourth is the name of the cache.  The cache is usually named after th API.
- * Some things you must know about the three strings:
- * <list>
- * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
- * Usually, the SELinux rules permit a process to write a system property (and therefore
- * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}.  This means that
- * although the cache can be constructed with any module string, whatever string is chosen must be
- * consistent with the SELinux configuration.
- * <ul> The API name can be any string of alphanumeric characters.  All caches with the same API
- * are invalidated at the same time.  If a server supports several caches and all are invalidated
- * in common, then it is most efficient to assign the same API string to every cache.
- * <ul> The cache name can be any string.  In debug output, the name is used to distiguish between
- * caches with the same API name.  The cache name is also used when disabling caches in the
- * current process.  So, invalidation is based on the module+api but disabling (which is generally
- * a once-per-process operation) is based on the cache name.
- * </list>
- *
- * User birthdays do occasionally change, so we have to modify the server to invalidate this
- * cache when necessary. That invalidation code looks like this:
- *
- * <pre>
- * public class UserBirthdayServiceImpl {
- *   ...
- *   public UserBirthdayServiceImpl() {
- *     ...
- *     ActivityThread.currentActivityThread().disableUserBirthdayCache();
- *     ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
- *   }
- *
- *   private synchronized void updateBirthdays(Map&lt;Integer, Birthday%&gt; uidToBirthday) {
- *     mUidToBirthday.clear();
- *     mUidToBirthday.putAll(uidToBirthday);
- *     ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
- *   }
- *   ...
- * }
- * </pre>
- *
- * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients
- * will re-fetch birthdays from binder during consequent calls to
- * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock
- * held, we maintain consistency between different client views of the birthday state. The use
- * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions.
- *
- * PropertyInvalidatedCache has a few other features for doing things like incremental
- * enhancement of cached values and invalidation of multiple caches (that all share the same
- * property key) at once.
- *
- * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each
- * time we update the cache. SELinux configuration must allow everyone to read this property
- * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write
- * the property. (These properties conventionally begin with the "cache_key." prefix.)
- *
- * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so
- * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In
- * this local case, there's no IPC, so use of the cache is (depending on exact
- * circumstance) unnecessary.
- *
- * There may be queries for which it is more efficient to bypass the cache than to cache
- * the result.  This would be true, for example, if some queries would require frequent
- * cache invalidation while other queries require infrequent invalidation.  To expand on
- * the birthday example, suppose that there is a userId that signifies "the next
- * birthday".  When passed this userId, the server returns the next birthday among all
- * users - this value changes as time advances.  The userId value can be cached, but the
- * cache must be invalidated whenever a birthday occurs, and this invalidates all
- * birthdays.  If there is a large number of users, invalidation will happen so often that
- * the cache provides no value.
- *
- * The class provides a bypass mechanism to handle this situation.
- * <pre>
- * public class ActivityThread {
- *   ...
- *   private final IpcDataCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
- *       new IpcDataCache.QueryHandler&lt;Integer, Birthday&gt;() {
- *           {@literal @}Override
- *           public Birthday apply(Integer) {
- *              return GetService("birthdayd").getUserBirthday(userId);
- *           }
- *           {@literal @}Override
- *           public boolean shouldBypassQuery(Integer userId) {
- *               return userId == NEXT_BIRTHDAY;
- *           }
- *       };
- *   ...
- * }
- * </pre>
- *
- * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
- * particular query.  The {@code shouldBypassQuery()} method is not abstract and the default
- * implementation returns false.
- *
- * For security, there is a allowlist of processes that are allowed to invalidate a cache.
- * The allowlist includes normal runtime processes but does not include test processes.
- * Test processes must call {@code PropertyInvalidatedCache.disableForTestMode()} to disable
- * all cache activity in that process.
- *
- * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding.
- *
- * To test a binder cache, create one or more tests that exercise the binder method.  This
- * should be done twice: once with production code and once with a special image that sets
- * {@code DEBUG} and {@code VERIFY} true.  In the latter case, verify that no cache
- * inconsistencies are reported.  If a cache inconsistency is reported, however, it might be a
- * false positive.  This happens if the server side data can be read and written non-atomically
- * with respect to cache invalidation.
+ * This interface is deprecated.  New clients should use {@link IpcDataCache} instead.  Internally,
+ * that class uses {@link PropertyInvalidatedCache} , but that design may change in the future.
  *
  * @param <Query> The class used to index cache entries: must be hashable and comparable
  * @param <Result> The class holding cache entries; use a boxed primitive if possible
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c13a58f..ea4148c 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -230,6 +230,7 @@
 import android.print.PrintManager;
 import android.provider.E2eeContactKeysManager;
 import android.provider.ProviderFrameworkInitializer;
+import android.ranging.RangingFrameworkInitializer;
 import android.safetycenter.SafetyCenterFrameworkInitializer;
 import android.scheduling.SchedulingFrameworkInitializer;
 import android.security.FileIntegrityManager;
@@ -1825,6 +1826,12 @@
             if (android.webkit.Flags.updateServiceIpcWrapper()) {
                 WebViewBootstrapFrameworkInitializer.registerServiceWrappers();
             }
+            // This is guarded by aconfig flag "com.android.ranging.flags.ranging_stack_enabled"
+            // when the build flag RELEASE_RANGING_STACK is enabled. When disabled, this calls the
+            // mock RangingFrameworkInitializer#registerServiceWrappers which is no-op. As the
+            // aconfig lib for ranging module is built only if  RELEASE_RANGING_STACK is enabled,
+            // flagcannot be added here.
+            RangingFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index daa15f0..9be928f 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -213,19 +213,17 @@
  * <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a>
  * developer guide.
  *
- * <p id="devicepolicycontroller">Through <a href="#managed_provisioning">Managed Provisioning</a>,
- * Device Administrator apps can also be recognised as <b>
- Device Policy Controllers</b>. Device Policy Controllers can be one of
+ * <p id="devicepolicycontroller">Device Administrator apps can also be recognised as <b>
+ * Device Policy Controllers</b>. Device Policy Controllers can be one of
  * two types:
  * <ul>
  * <li>A <i id="deviceowner">Device Owner</i>, which only ever exists on the
- * {@link UserManager#isSystemUser System User} or {@link UserManager#isMainUser Main User}, is
+ * {@link UserManager#isSystemUser System User} or Main User, is
  * the most powerful type of Device Policy Controller and can affect policy across the device.
  * <li>A <i id="profileowner">Profile Owner<i>, which can exist on any user, can
  * affect policy on the user it is on, and when it is running on
  * {@link UserManager#isProfile a profile} has
- * <a href="#profile-on-parent">limited</a> ability to affect policy on its
- * {@link UserManager#getProfileParent parent}.
+ * <a href="#profile-on-parent">limited</a> ability to affect policy on its parent.
  * </ul>
  *
  * <p>Additional capabilities can be provided to Device Policy Controllers in
@@ -233,7 +231,7 @@
  * <ul>
  * <li>A Profile Owner on an <a href="#organization-owned">organization owned</a> device has access
  * to additional abilities, both <a href="#profile-on-parent-organization-owned">affecting policy on the profile's</a>
- * {@link UserManager#getProfileParent parent} and also the profile itself.
+ * parent and also the profile itself.
  * <li>A Profile Owner running on the {@link UserManager#isSystemUser System User} has access to
  * additional capabilities which affect the {@link UserManager#isSystemUser System User} and
  * also the whole device.
@@ -245,13 +243,12 @@
  * Controller</a>.
  *
  * <p><a href="#permissions">Permissions</a> are generally only given to apps
- * fulfilling particular key roles on the device (such as managing {@link DeviceLockManager
-device locks}).
+ * fulfilling particular key roles on the device (such as managing
+ * {@link android.devicelock.DeviceLockManager device locks}).
  *
  * <p id="roleholder"><b>Device Policy Management Role Holder</b>
- * <p>One app on the device fulfills the {@link RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT Device
-Policy Management Role} and is trusted with managing the overall state of
- * Device Policy. This has access to much more powerful methods than
+ * <p>One app on the device fulfills the Device Policy Management Role and is trusted with managing
+ * the overall state of Device Policy. This has access to much more powerful methods than
  * <a href="#managingapps">managing apps</a>.
  *
  * <p id="querying"><b>Querying Device Policy</b>
@@ -273,7 +270,7 @@
  *
  * <p id="managed_profile">A <b>Managed Profile</b> enables data separation. For example to use
  * a device both for personal and corporate usage. The managed profile and its
- * {@link UserManager#getProfileParent parent} share a launcher.
+ * parent share a launcher.
  *
  * <p id="affiliated"><b>Affiliation</b>
  * <p>Using the {@link #setAffiliationIds} method, a
@@ -6643,7 +6640,7 @@
      * @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}.
      * @throws SecurityException if the calling application does not own an active administrator
      *             that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} and the does not hold
-     *             the {@link android.Manifest.permission#LOCK_DEVICE} permission, or
+     *             the LOCK_DEVICE permission, or
      *             the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an
      *             application that is not a profile owner of a managed profile.
      * @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 4682f3d..216ba5d 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,8 @@
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
 import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.ICancellationSignal;
 import android.os.RemoteException;
 
 import java.util.Objects;
@@ -73,7 +75,43 @@
      *     android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
      *     android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
      *     ExecuteAppFunctionResponse.RESULT_DENIED}.
+     * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+     *     CancellationSignal, Consumer)} instead. This method will be removed once usage references
+     *     are updated.
      */
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
+                Manifest.permission.EXECUTE_APP_FUNCTIONS
+            },
+            conditional = true)
+    @UserHandleAware
+    @Deprecated
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        executeAppFunction(request, executor, new CancellationSignal(), callback);
+    }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Note: Applications can execute functions they define. To execute functions defined in
+     * another component, apps would need to have {@code
+     * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+     * android.permission.EXECUTE_APP_FUNCTIONS}.
+     *
+     * @param request the request to execute the app function
+     * @param executor the executor to run the callback
+     * @param cancellationSignal the cancellation signal to cancel the execution.
+     * @param callback the callback to receive the function execution result. if the calling app
+     *     does not own the app function or does not have {@code
+     *     android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+     *     android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
+     *     ExecuteAppFunctionResponse.RESULT_DENIED}.
+     */
+    // TODO(b/357551503): Document the behavior when the cancellation signal is issued.
     // TODO(b/360864791): Document that apps can opt-out from being executed by callers with
     //   EXECUTE_APP_FUNCTIONS and how a caller knows whether a function is opted out.
     // TODO(b/357551503): Update documentation when get / set APIs are implemented that this will
@@ -88,6 +126,7 @@
     public void executeAppFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull @CallbackExecutor Executor executor,
+            @NonNull CancellationSignal cancellationSignal,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
         Objects.requireNonNull(request);
         Objects.requireNonNull(executor);
@@ -96,25 +135,31 @@
         ExecuteAppFunctionAidlRequest aidlRequest =
                 new ExecuteAppFunctionAidlRequest(
                         request, mContext.getUser(), mContext.getPackageName());
+
         try {
-            mService.executeAppFunction(
-                    aidlRequest,
-                    new IExecuteAppFunctionCallback.Stub() {
-                        @Override
-                        public void onResult(ExecuteAppFunctionResponse result) {
-                            try {
-                                executor.execute(() -> callback.accept(result));
-                            } catch (RuntimeException e) {
-                                // Ideally shouldn't happen since errors are wrapped into the
-                                // response, but we catch it here for additional safety.
-                                callback.accept(
-                                        ExecuteAppFunctionResponse.newFailure(
-                                                getResultCode(e),
-                                                e.getMessage(),
-                                                /* extras= */ null));
-                            }
-                        }
-                    });
+            ICancellationSignal cancellationTransport =
+                    mService.executeAppFunction(
+                            aidlRequest,
+                            new IExecuteAppFunctionCallback.Stub() {
+                                @Override
+                                public void onResult(ExecuteAppFunctionResponse result) {
+                                    try {
+                                        executor.execute(() -> callback.accept(result));
+                                    } catch (RuntimeException e) {
+                                        // Ideally shouldn't happen since errors are wrapped into
+                                        // the
+                                        // response, but we catch it here for additional safety.
+                                        callback.accept(
+                                                ExecuteAppFunctionResponse.newFailure(
+                                                        getResultCode(e),
+                                                        e.getMessage(),
+                                                        /* extras= */ null));
+                                    }
+                                }
+                            });
+            if (cancellationTransport != null) {
+                cancellationSignal.setRemote(cancellationTransport);
+            }
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 0d981ea..8e41773 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -29,7 +29,12 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.CancellationSignal;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
 
 import java.util.function.Consumer;
 
@@ -74,6 +79,7 @@
          */
         void perform(
                 @NonNull ExecuteAppFunctionRequest request,
+                @NonNull CancellationSignal cancellationSignal,
                 @NonNull Consumer<ExecuteAppFunctionResponse> callback);
     }
 
@@ -85,6 +91,7 @@
             @Override
             public void executeAppFunction(
                     @NonNull ExecuteAppFunctionRequest request,
+                    @NonNull ICancellationCallback cancellationCallback,
                     @NonNull IExecuteAppFunctionCallback callback) {
                 if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
                         == PERMISSION_DENIED) {
@@ -93,7 +100,10 @@
                 SafeOneTimeExecuteAppFunctionCallback safeCallback =
                         new SafeOneTimeExecuteAppFunctionCallback(callback);
                 try {
-                    onExecuteFunction.perform(request, safeCallback::onResult);
+                    onExecuteFunction.perform(
+                            request,
+                            buildCancellationSignal(cancellationCallback),
+                            safeCallback::onResult);
                 } catch (Exception ex) {
                     // Apps should handle exceptions. But if they don't, report the error on
                     // behalf of them.
@@ -105,6 +115,21 @@
         };
     }
 
+    private static CancellationSignal buildCancellationSignal(
+            @NonNull ICancellationCallback cancellationCallback) {
+        final ICancellationSignal cancellationSignalTransport =
+                CancellationSignal.createTransport();
+        CancellationSignal cancellationSignal =
+                CancellationSignal.fromTransport(cancellationSignalTransport);
+        try {
+            cancellationCallback.sendCancellationTransport(cancellationSignalTransport);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return cancellationSignal ;
+    }
+
     private final Binder mBinder = createBinder(
             AppFunctionService.this,
             AppFunctionService.this::onExecuteFunction);
@@ -115,6 +140,7 @@
         return mBinder;
     }
 
+
     /**
      * Called by the system to execute a specific app function.
      *
@@ -134,9 +160,45 @@
      *
      * @param request The function execution request.
      * @param callback A callback to report back the result.
+     *
+     * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+     *     Consumer)} instead. This method will be removed once usage references are updated.
      */
     @MainThread
+    @Deprecated
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
+     * the execution of function if requested by the system.
+     *
+     * @param request The function execution request.
+     * @param cancellationSignal A signal to cancel the execution.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        onExecuteFunction(request, callback);
+    }
 }
diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
index 84b1837..b29b64e 100644
--- a/core/java/android/app/appfunctions/GenericDocumentWrapper.java
+++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
@@ -16,10 +16,13 @@
 
 package android.app.appfunctions;
 
+import android.annotation.Nullable;
 import android.app.appsearch.GenericDocument;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.MathUtils;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 
 import java.util.Objects;
@@ -31,24 +34,33 @@
  * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder
  * directly or Android shared memory if the data is large.
  *
+ * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled
+ * from the underlying `Parcel` when {@link #getValue()} is called. This optimization
+ * allows the system server to pass through the generic document, without unparcel and parcel it.
+ *
  * @hide
  * @see Parcel#writeBlob(byte[])
  */
 public final class GenericDocumentWrapper implements Parcelable {
+    @Nullable
+    @GuardedBy("mLock")
+    private GenericDocument mGenericDocument;
+    @GuardedBy("mLock")
+    @Nullable private Parcel mParcel;
+    private final Object mLock = new Object();
+
     public static final Creator<GenericDocumentWrapper> CREATOR =
             new Creator<>() {
                 @Override
                 public GenericDocumentWrapper createFromParcel(Parcel in) {
-                    byte[] dataBlob = Objects.requireNonNull(in.readBlob());
-                    Parcel unmarshallParcel = Parcel.obtain();
-                    try {
-                        unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
-                        unmarshallParcel.setDataPosition(0);
-                        return new GenericDocumentWrapper(
-                                GenericDocument.createFromParcel(unmarshallParcel));
-                    } finally {
-                        unmarshallParcel.recycle();
-                    }
+                    int length = in.readInt();
+                    int offset = in.dataPosition();
+                    in.setDataPosition(MathUtils.addOrThrow(offset, length));
+
+                    Parcel p = Parcel.obtain();
+                    p.appendFrom(in, offset, length);
+                    p.setDataPosition(0);
+                    return new GenericDocumentWrapper(p);
                 }
 
                 @Override
@@ -56,16 +68,42 @@
                     return new GenericDocumentWrapper[size];
                 }
             };
-    @NonNull private final GenericDocument mGenericDocument;
 
     public GenericDocumentWrapper(@NonNull GenericDocument genericDocument) {
         mGenericDocument = Objects.requireNonNull(genericDocument);
+        mParcel = null;
+    }
+
+    public GenericDocumentWrapper(@NonNull Parcel parcel) {
+        mGenericDocument = null;
+        mParcel = Objects.requireNonNull(parcel);
     }
 
     /** Returns the wrapped {@link android.app.appsearch.GenericDocument} */
     @NonNull
     public GenericDocument getValue() {
-        return mGenericDocument;
+        unparcel();
+        synchronized (mLock) {
+            return Objects.requireNonNull(mGenericDocument);
+        }
+    }
+
+    private void unparcel() {
+        synchronized (mLock) {
+            if (mGenericDocument != null) {
+                return;
+            }
+            byte[] dataBlob = Objects.requireNonNull(Objects.requireNonNull(mParcel).readBlob());
+            Parcel unmarshallParcel = Parcel.obtain();
+            try {
+                unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+                unmarshallParcel.setDataPosition(0);
+                mGenericDocument = GenericDocument.createFromParcel(unmarshallParcel);
+                mParcel = null;
+            } finally {
+                unmarshallParcel.recycle();
+            }
+        }
     }
 
     @Override
@@ -75,13 +113,32 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        Parcel parcel = Parcel.obtain();
-        try {
-            mGenericDocument.writeToParcel(parcel, flags);
-            byte[] bytes = parcel.marshall();
-            dest.writeBlob(bytes);
-        } finally {
-            parcel.recycle();
+        synchronized (mLock) {
+            if (mGenericDocument != null) {
+                int lengthPos = dest.dataPosition();
+                // write a placeholder for length
+                dest.writeInt(-1);
+                Parcel tempParcel = Parcel.obtain();
+                byte[] bytes;
+                try {
+                    mGenericDocument.writeToParcel(tempParcel, flags);
+                    bytes = tempParcel.marshall();
+                } finally {
+                    tempParcel.recycle();
+                }
+                int startPos = dest.dataPosition();
+                dest.writeBlob(bytes);
+                int endPos = dest.dataPosition();
+                dest.setDataPosition(lengthPos);
+                // Overwrite the length placeholder
+                dest.writeInt(endPos - startPos);
+                dest.setDataPosition(endPos);
+
+            } else {
+                Parcel originalParcel = Objects.requireNonNull(mParcel);
+                dest.writeInt(originalParcel.dataSize());
+                dest.appendFrom(originalParcel, 0, originalParcel.dataSize());
+            }
         }
     }
 }
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index 28827bb..c63217f 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -18,6 +18,7 @@
 
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.os.ICancellationSignal;
 
 /**
  * Defines the interface for apps to interact with the app function execution service
@@ -32,8 +33,8 @@
     * @param callback the callback to report the result.
     */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)")
-    void executeAppFunction(
+    ICancellationSignal executeAppFunction(
         in ExecuteAppFunctionAidlRequest request,
         in IExecuteAppFunctionCallback callback
     );
-}
\ No newline at end of file
+}
diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
index cc5a20c..291f33c 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -16,7 +16,7 @@
 
 package android.app.appfunctions;
 
-import android.os.Bundle;
+import android.app.appfunctions.ICancellationCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.ExecuteAppFunctionRequest;
 
@@ -34,10 +34,12 @@
      * Called by the system to execute a specific app function.
      *
      * @param request  the function execution request.
+     * @param cancellationCallback a callback to send back the cancellation transport.
      * @param callback a callback to report back the result.
      */
     void executeAppFunction(
         in ExecuteAppFunctionRequest request,
+        in ICancellationCallback cancellationCallback,
         in IExecuteAppFunctionCallback callback
     );
 }
diff --git a/core/java/android/app/appfunctions/ICancellationCallback.aidl b/core/java/android/app/appfunctions/ICancellationCallback.aidl
new file mode 100644
index 0000000..03235ac
--- /dev/null
+++ b/core/java/android/app/appfunctions/ICancellationCallback.aidl
@@ -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 android.app.appfunctions;
+
+import android.os.ICancellationSignal;
+
+/** {@hide} */
+oneway interface ICancellationCallback {
+    void sendCancellationTransport(in ICancellationSignal cancellationTransport);
+}
\ No newline at end of file
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 108b5f4..b139017 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -31,6 +31,16 @@
 }
 
 flag {
+  name: "modes_ui_empty_shade"
+  namespace: "systemui"
+  description: "Shows mode that is currently blocking notifications in the empty shade; dependent on flags modes_api and modes_ui"
+  bug: "366003631"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "modes_ui_test"
   namespace: "systemui"
   description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 8d25cad..4598421 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -21,5 +21,5 @@
  * {@hide}
  */
 interface ISupervisionManager {
-    boolean isSupervisionEnabled();
+    boolean isSupervisionEnabledForUser(int userId);
 }
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 8611a92..aee1cd9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -17,6 +17,7 @@
 package android.app.supervision;
 
 import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.os.RemoteException;
@@ -45,13 +46,12 @@
      *
      * @hide
      */
+    @UserHandleAware
     public boolean isSupervisionEnabled() {
         try {
-            return mService.isSupervisionEnabled();
+            return mService.isSupervisionEnabledForUser(mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
-
-
 }
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index abb562d..d8142fd 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -1042,10 +1042,11 @@
     }
 
     /**
-     * Get the available info about the AppWidget.
+     * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget.
      *
-     * @return A appWidgetId.  If the appWidgetId has not been bound to a provider yet, or
-     * you don't have access to that appWidgetId, null is returned.
+     * @return Information regarding the provider of speficied widget, returns null if the
+     *         appWidgetId has not been bound to a provider yet, or you don't have access
+     *         to that widget.
      */
     public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) {
         if (mService == null) {
@@ -1390,7 +1391,7 @@
      *
      * @param provider The {@link ComponentName} for the {@link
      *    android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
-     * @param extras In not null, this is passed to the launcher app. For eg {@link
+     * @param extras IF not null, this is passed to the launcher app. e.g. {@link
      *    #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview.
      * @param successCallback If not null, this intent will be sent when the widget is created.
      *
diff --git a/core/java/android/content/pm/SharedLibraryInfo.java b/core/java/android/content/pm/SharedLibraryInfo.java
index 5acebf5..d77b2f5 100644
--- a/core/java/android/content/pm/SharedLibraryInfo.java
+++ b/core/java/android/content/pm/SharedLibraryInfo.java
@@ -94,7 +94,7 @@
     private final String mPath;
     private final String mPackageName;
     private final String mName;
-    private final List<String> mCodePaths;
+    private List<String> mCodePaths;
 
     private final long mVersion;
     private final @Type int mType;
@@ -282,6 +282,15 @@
     }
 
     /**
+     * Sets new all code paths for that library.
+     *
+     * @hide
+     */
+    public void setAllCodePaths(List<String> paths) {
+        mCodePaths = paths;
+    }
+
+    /**
      * Add a library dependency to that library. Note that this
      * should be called under the package manager lock.
      *
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 139ff65..160cbdf 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -293,4 +293,19 @@
     description: "Feature flag to provide the new methods within launcher apps class to get packages."
     bug: "363324203"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+    name: "remove_cross_user_permission_hack"
+    namespace: "package_manager_service"
+    description: "Feature flag to remove hack code of using PackageManager.MATCH_ANY_USER flag without cross user permission."
+    bug: "332664521"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "delete_packages_silently_backport"
+    namespace: "package_manager_service"
+    description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently."
+    bug: "361776825"
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 2162792..1b21bdf 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -181,7 +181,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE;
 
     /**
@@ -191,7 +191,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT =
             ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT;
 
@@ -201,7 +201,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_ROTATION_ONLY =
             ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY;
 
@@ -1562,7 +1562,7 @@
      */
     public static int getRotationOverride(@Nullable Context context,
             @Nullable PackageManager packageManager, @Nullable String packageName) {
-        if (com.android.window.flags.Flags.cameraCompatForFreeform()) {
+        if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) {
             return getRotationOverrideInternal(context, packageManager, packageName);
         } else {
             return shouldOverrideToPortrait(packageManager, packageName)
@@ -1574,7 +1574,7 @@
     /**
      * @hide
      */
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @TestApi
     public static int getRotationOverrideInternal(@Nullable Context context,
             @Nullable PackageManager packageManager, @Nullable String packageName) {
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index a2d24f6..73b5d94 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -432,6 +432,11 @@
     public abstract IntArray getDisplayGroupIds();
 
     /**
+     * Get all available display ids.
+     */
+    public abstract IntArray getDisplayIds();
+
+    /**
      * Called upon presentation started/ended on the display.
      * @param displayId the id of the display where presentation started.
      * @param isShown whether presentation is shown.
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index bdbec55..5ee61bc 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -48,49 +48,57 @@
     public static final int KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL = 8;
     public static final int KEY_GESTURE_TYPE_TOGGLE_TASKBAR = 9;
     public static final int KEY_GESTURE_TYPE_TAKE_SCREENSHOT = 10;
-    public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 11;
-    public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 12;
-    public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 13;
-    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 14;
-    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 15;
-    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 16;
-    public static final int KEY_GESTURE_TYPE_VOLUME_UP = 17;
-    public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 18;
-    public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 19;
-    public static final int KEY_GESTURE_TYPE_ALL_APPS = 20;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 21;
-    public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 22;
-    public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 23;
-    public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 24;
-    public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 25;
-    public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 26;
-    public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 27;
-    public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 28;
-    public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 29;
-    public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 30;
-    public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 31;
-    public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 32;
-    public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 33;
-    public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 34;
-    public static final int KEY_GESTURE_TYPE_SLEEP = 35;
-    public static final int KEY_GESTURE_TYPE_WAKEUP = 36;
-    public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 37;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 38;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 39;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 40;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 41;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 42;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 43;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 44;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 45;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 46;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 47;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 48;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 49;
-    public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 50;
-    public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 51;
-    public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 52;
-    public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 53;
+    public static final int KEY_GESTURE_TYPE_SCREENSHOT_CHORD = 11;
+    public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 12;
+    public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 13;
+    public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 14;
+    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 15;
+    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 16;
+    public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 17;
+    public static final int KEY_GESTURE_TYPE_VOLUME_UP = 18;
+    public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 19;
+    public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 20;
+    public static final int KEY_GESTURE_TYPE_ALL_APPS = 21;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22;
+    public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23;
+    public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24;
+    public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25;
+    public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26;
+    public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27;
+    public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 28;
+    public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 29;
+    public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 30;
+    public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 31;
+    public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 32;
+    public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 33;
+    public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 34;
+    public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 35;
+    public static final int KEY_GESTURE_TYPE_SLEEP = 36;
+    public static final int KEY_GESTURE_TYPE_WAKEUP = 37;
+    public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 38;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 39;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 40;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 41;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 42;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 43;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 44;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 45;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 46;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 47;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 48;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 49;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 50;
+    public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 51;
+    public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 52;
+    public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 53;
+    public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 54;
+    public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
+    public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
+    public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
+    public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+    public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
+    public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
+    public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -116,6 +124,7 @@
             KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
             KEY_GESTURE_TYPE_TOGGLE_TASKBAR,
             KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+            KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
             KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
             KEY_GESTURE_TYPE_BRIGHTNESS_UP,
             KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
@@ -158,7 +167,15 @@
             KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME,
             KEY_GESTURE_TYPE_DESKTOP_MODE,
             KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
-            KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+            KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
+            KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+            KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
+            KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
+            KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+            KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+            KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+            KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
@@ -360,6 +377,7 @@
             case KEY_GESTURE_TYPE_TOGGLE_TASKBAR:
                 return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR;
             case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
+            case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
                 return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT;
             case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
                 return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER;
@@ -472,6 +490,8 @@
                 return "KEY_GESTURE_TYPE_TOGGLE_TASKBAR";
             case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
                 return "KEY_GESTURE_TYPE_TAKE_SCREENSHOT";
+            case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+                return "KEY_GESTURE_TYPE_SCREENSHOT_CHORD";
             case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
                 return "KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER";
             case KEY_GESTURE_TYPE_BRIGHTNESS_UP:
@@ -558,6 +578,20 @@
                 return "KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION";
             case KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
                 return "KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER";
+            case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+                return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD";
+            case KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+                return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
+            case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+                return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
+            case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+                return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
+            case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+                return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
+            case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+                return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT";
+            case KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+                return "KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java
index 5c07fa4..22ae676 100644
--- a/core/java/android/hardware/soundtrigger/ConversionUtil.java
+++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java
@@ -155,16 +155,16 @@
     public static RecognitionConfig api2aidlRecognitionConfig(
             SoundTrigger.RecognitionConfig apiConfig) {
         RecognitionConfig aidlConfig = new RecognitionConfig();
-        aidlConfig.captureRequested = apiConfig.captureRequested;
-        // apiConfig.allowMultipleTriggers is ignored by the lower layers.
+        aidlConfig.captureRequested = apiConfig.isCaptureRequested();
+        // apiConfig.isAllowMultipleTriggers() is ignored by the lower layers.
         aidlConfig.phraseRecognitionExtras =
-                new PhraseRecognitionExtra[apiConfig.keyphrases.length];
-        for (int i = 0; i < apiConfig.keyphrases.length; ++i) {
+                new PhraseRecognitionExtra[apiConfig.getKeyphrases().size()];
+        for (int i = 0; i < apiConfig.getKeyphrases().size(); ++i) {
             aidlConfig.phraseRecognitionExtras[i] = api2aidlPhraseRecognitionExtra(
-                    apiConfig.keyphrases[i]);
+                    apiConfig.getKeyphrases().get(i));
         }
-        aidlConfig.data = Arrays.copyOf(apiConfig.data, apiConfig.data.length);
-        aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.audioCapabilities);
+        aidlConfig.data = Arrays.copyOf(apiConfig.getData(), apiConfig.getData().length);
+        aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.getAudioCapabilities());
         return aidlConfig;
     }
 
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 9f3e3ad..05e91e4 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -63,6 +63,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.UUID;
@@ -1513,50 +1514,60 @@
      *  A RecognitionConfig is provided to
      *  {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} to configure the
      *  recognition request.
-     *
-     *  @hide
      */
-    @TestApi
+    @FlaggedApi(android.media.soundtrigger.Flags.FLAG_MANAGER_API)
     public static final class RecognitionConfig implements Parcelable {
-        /** True if the DSP should capture the trigger sound and make it available for further
-         * capture. */
-        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-        public final boolean captureRequested;
-        /**
-         * True if the service should restart listening after the DSP triggers.
-         * Note: This config flag is currently used at the service layer rather than by the DSP.
-         */
-        public final boolean allowMultipleTriggers;
-        /** List of all keyphrases in the sound model for which recognition should be performed with
-         * options for each keyphrase. */
-        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-        @NonNull
-        @SuppressLint("ArrayReturn")
-        public final KeyphraseRecognitionExtra keyphrases[];
-        /** Opaque data for use by system applications who know about voice engine internals,
-         * typically during enrollment. */
-        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-        @NonNull
-        public final byte[] data;
-
-        /**
-         * Bit field encoding of the AudioCapabilities
-         * supported by the firmware.
-         */
+        private final boolean mCaptureRequested;
+        private final boolean mAllowMultipleTriggers;
+        private final KeyphraseRecognitionExtra mKeyphrases[];
+        private final byte[] mData;
         @ModuleProperties.AudioCapabilities
-        public final int audioCapabilities;
+        private final int mAudioCapabilities;
 
+        /**
+         * Constructor for {@link RecognitionConfig} with {@code audioCapabilities} describes a
+         * config that can be used by
+         * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)}
+         *
+         * @deprecated should use builder-based constructor instead.
+         *             TODO(b/368042125): remove this method.
+         * @param captureRequested Whether the DSP should capture the trigger sound.
+         * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+         *                              triggers.
+         * @param keyphrases List of keyphrases in the sound model.
+         * @param data Opaque data for use by system applications who know about voice engine
+         *             internals, typically during enrollment.
+         * @param audioCapabilities Bit field encoding of the AudioCapabilities.
+         *
+         * @hide
+         */
+        @Deprecated
+        @SuppressWarnings("Todo")
+        @TestApi
         public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
                 @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
                 @Nullable byte[] data, int audioCapabilities) {
-            this.captureRequested = captureRequested;
-            this.allowMultipleTriggers = allowMultipleTriggers;
-            this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
-            this.data = data != null ? data : new byte[0];
-            this.audioCapabilities = audioCapabilities;
+            this.mCaptureRequested = captureRequested;
+            this.mAllowMultipleTriggers = allowMultipleTriggers;
+            this.mKeyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
+            this.mData = data != null ? data : new byte[0];
+            this.mAudioCapabilities = audioCapabilities;
         }
 
+        /**
+         * Constructor for {@link RecognitionConfig} without audioCapabilities. The
+         * audioCapabilities is set to 0.
+         *
+         * @param captureRequested Whether the DSP should capture the trigger sound.
+         * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+         *                              triggers.
+         * @param keyphrases List of keyphrases in the sound model.
+         * @param data Opaque data for use by system applications.
+         *
+         * @hide
+         */
         @UnsupportedAppUsage
+        @TestApi
         public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
                 @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
                 @Nullable byte[] data) {
@@ -1574,9 +1585,52 @@
             }
         };
 
+        /**
+         * Returns whether the DSP should capture the trigger sound and make it available for
+         * further capture.
+         */
+        public boolean isCaptureRequested() {
+            return mCaptureRequested;
+        }
+
+        /**
+         * Returns whether the service should restart listening after the DSP triggers.
+         *
+         * <p><b>Note:</b> This config flag is currently used at the service layer rather than by
+         * the DSP.
+         */
+        public boolean isAllowMultipleTriggers() {
+            return mAllowMultipleTriggers;
+        }
+
+        /**
+         * Gets all keyphrases in the sound model for which recognition should be performed with
+         * options for each keyphrase.
+         */
+        @NonNull
+        public List<KeyphraseRecognitionExtra> getKeyphrases() {
+            return Arrays.asList(mKeyphrases);
+        }
+
+        /**
+         * Opaque data.
+         *
+         * <p>For use by system applications who knows about voice engine internals, typically
+         * during enrollment.
+         */
+        @NonNull
+        public byte[] getData() {
+            return mData;
+        }
+
+        /** Bit field encoding of the AudioCapabilities supported by the firmware. */
+        public int getAudioCapabilities() {
+            return mAudioCapabilities;
+        }
+
         private static RecognitionConfig fromParcel(Parcel in) {
-            boolean captureRequested = in.readByte() == 1;
-            boolean allowMultipleTriggers = in.readByte() == 1;
+            boolean captureRequested = in.readBoolean();
+            boolean allowMultipleTriggers = in.readBoolean();
             KeyphraseRecognitionExtra[] keyphrases =
                     in.createTypedArray(KeyphraseRecognitionExtra.CREATOR);
             byte[] data = in.readBlob();
@@ -1587,11 +1641,11 @@
 
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
-            dest.writeByte((byte) (captureRequested ? 1 : 0));
-            dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0));
-            dest.writeTypedArray(keyphrases, flags);
-            dest.writeBlob(data);
-            dest.writeInt(audioCapabilities);
+            dest.writeBoolean(mCaptureRequested);
+            dest.writeBoolean(mAllowMultipleTriggers);
+            dest.writeTypedArray(mKeyphrases, flags);
+            dest.writeBlob(mData);
+            dest.writeInt(mAudioCapabilities);
         }
 
         @Override
@@ -1601,10 +1655,10 @@
 
         @Override
         public String toString() {
-            return "RecognitionConfig [captureRequested=" + captureRequested
-                    + ", allowMultipleTriggers=" + allowMultipleTriggers + ", keyphrases="
-                    + Arrays.toString(keyphrases) + ", data=" + Arrays.toString(data)
-                    + ", audioCapabilities=" + Integer.toHexString(audioCapabilities) + "]";
+            return "RecognitionConfig [captureRequested=" + mCaptureRequested
+                    + ", allowMultipleTriggers=" + mAllowMultipleTriggers + ", keyphrases="
+                    + Arrays.toString(mKeyphrases) + ", data=" + Arrays.toString(mData)
+                    + ", audioCapabilities=" + Integer.toHexString(mAudioCapabilities) + "]";
         }
 
         @Override
@@ -1616,19 +1670,19 @@
             if (!(obj instanceof RecognitionConfig))
                 return false;
             RecognitionConfig other = (RecognitionConfig) obj;
-            if (captureRequested != other.captureRequested) {
+            if (mCaptureRequested != other.mCaptureRequested) {
                 return false;
             }
-            if (allowMultipleTriggers != other.allowMultipleTriggers) {
+            if (mAllowMultipleTriggers != other.mAllowMultipleTriggers) {
                 return false;
             }
-            if (!Arrays.equals(keyphrases, other.keyphrases)) {
+            if (!Arrays.equals(mKeyphrases, other.mKeyphrases)) {
                 return false;
             }
-            if (!Arrays.equals(data, other.data)) {
+            if (!Arrays.equals(mData, other.mData)) {
                 return false;
             }
-            if (audioCapabilities != other.audioCapabilities) {
+            if (mAudioCapabilities != other.mAudioCapabilities) {
                 return false;
             }
             return true;
@@ -1638,13 +1692,96 @@
         public final int hashCode() {
             final int prime = 31;
             int result = 1;
-            result = prime * result + (captureRequested ? 1 : 0);
-            result = prime * result + (allowMultipleTriggers ? 1 : 0);
-            result = prime * result + Arrays.hashCode(keyphrases);
-            result = prime * result + Arrays.hashCode(data);
-            result = prime * result + audioCapabilities;
+            result = prime * result + (mCaptureRequested ? 1 : 0);
+            result = prime * result + (mAllowMultipleTriggers ? 1 : 0);
+            result = prime * result + Arrays.hashCode(mKeyphrases);
+            result = prime * result + Arrays.hashCode(mData);
+            result = prime * result + mAudioCapabilities;
             return result;
         }
+
+        /**
+         * Builder class for {@link RecognitionConfig} objects.
+         */
+        public static final class Builder {
+            private boolean mCaptureRequested;
+            private boolean mAllowMultipleTriggers;
+            @Nullable private KeyphraseRecognitionExtra[] mKeyphrases;
+            @Nullable private byte[] mData;
+            private int mAudioCapabilities;
+
+            /**
+             * Constructs a new Builder with the default values.
+             */
+            public Builder() {
+            }
+
+            /**
+             * Sets capture requested state.
+             * @param captureRequested The new requested state.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setCaptureRequested(boolean captureRequested) {
+                mCaptureRequested = captureRequested;
+                return this;
+            }
+
+            /**
+             * Sets allow multiple triggers state.
+             * @param allowMultipleTriggers The new allow multiple triggers state.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setAllowMultipleTriggers(boolean allowMultipleTriggers) {
+                mAllowMultipleTriggers = allowMultipleTriggers;
+                return this;
+            }
+
+            /**
+             * Sets the keyphrases field.
+             * @param keyphrases The new keyphrases.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setKeyphrases(
+                    @NonNull Collection<KeyphraseRecognitionExtra> keyphrases) {
+                mKeyphrases = keyphrases.toArray(new KeyphraseRecognitionExtra[keyphrases.size()]);
+                return this;
+            }
+
+            /**
+             * Sets the data field.
+             * @param data The new data.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setData(@Nullable byte[] data) {
+                mData = data;
+                return this;
+            }
+
+            /**
+             * Sets the audio capabilities field.
+             * @param audioCapabilities The new audio capabilities.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setAudioCapabilities(int audioCapabilities) {
+                mAudioCapabilities = audioCapabilities;
+                return this;
+            }
+
+            /**
+             * Combines all of the parameters that have been set and return a new
+             * {@link RecognitionConfig} object.
+             * @return a new {@link RecognitionConfig} object
+             */
+            public @NonNull RecognitionConfig build() {
+                RecognitionConfig config = new RecognitionConfig(
+                        /* captureRequested= */ mCaptureRequested,
+                        /* allowMultipleTriggers= */ mAllowMultipleTriggers,
+                        /* keyphrases= */ mKeyphrases,
+                        /* data= */ mData,
+                        /* audioCapabilities= */ mAudioCapabilities);
+                return config;
+            }
+        };
     }
 
     /**
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 91cdf8d..1c9be6f 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -76,11 +76,15 @@
  * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is
  * absent, {@link Context#getSystemService} may return null.
  */
-@SystemService(Context.VCN_MANAGEMENT_SERVICE)
+@SystemService(VcnManager.VCN_MANAGEMENT_SERVICE_STRING)
 @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
 public class VcnManager {
     @NonNull private static final String TAG = VcnManager.class.getSimpleName();
 
+    // TODO: b/366598445: Expose and use Context.VCN_MANAGEMENT_SERVICE
+    /** @hide */
+    public static final String VCN_MANAGEMENT_SERVICE_STRING = "vcn_management";
+
     /**
      * Key for WiFi entry RSSI thresholds
      *
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 1fef602..a698b9d 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -788,6 +788,12 @@
     /** Parses an XML representation of BatteryUsageStats */
     public static BatteryUsageStats createFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
+        return createBuilderFromXml(parser).build();
+    }
+
+    /** Parses an XML representation of BatteryUsageStats */
+    public static BatteryUsageStats.Builder createBuilderFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
         Builder builder = null;
         int eventType = parser.getEventType();
         while (eventType != XmlPullParser.END_DOCUMENT) {
@@ -862,7 +868,7 @@
             eventType = parser.next();
         }
 
-        return builder.build();
+        return builder;
     }
 
     @Override
@@ -978,10 +984,21 @@
          */
         @NonNull
         public BatteryUsageStats build() {
+            if (mBatteryConsumersCursorWindow == null) {
+                throw new IllegalStateException("Builder has been discarded");
+            }
             return new BatteryUsageStats(this);
         }
 
         /**
+         * Close this builder without actually calling ".build()". Do not attempt
+         * to continue using the builder after this call.
+         */
+        public void discard() {
+            mBatteryConsumersCursorWindow.close();
+        }
+
+        /**
          * Sets the battery capacity in milli-amp-hours.
          */
         public Builder setBatteryCapacity(double batteryCapacityMah) {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index a12606b..b533225 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -77,6 +77,8 @@
 
     public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040;
 
+    public static final int FLAG_BATTERY_USAGE_STATS_ACCUMULATED = 0x0080;
+
     private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
 
     private final int mFlags;
@@ -328,6 +330,15 @@
         }
 
         /**
+         * Requests the full continuously accumulated battery usage stats: across reboots
+         * and most battery stats resets.
+         */
+        public Builder accumulated() {
+            mFlags |= FLAG_BATTERY_USAGE_STATS_ACCUMULATED;
+            return this;
+        }
+
+        /**
          * Requests to aggregate stored snapshots between the two supplied timestamps
          * @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
          * @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 97e9f34..ed75491 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1111,6 +1111,21 @@
     }
 
     /**
+     * Called whenever the stub implementation throws an exception which isn't propagated to the
+     * remote caller by the binder. If this method isn't overridden, this exception is swallowed,
+     * and some default return values are propagated to the caller.
+     *
+     * <br> <b> This should not throw. </b> Doing so would defeat the purpose of this handler, and
+     * suppress the exception it is handling.
+     *
+     * @param code The transaction code being handled
+     * @param e The exception which was thrown.
+     * @hide
+     */
+    protected void onUnhandledException(int code, int flags, Exception e) {
+    }
+
+    /**
      * @param in The raw file descriptor that an input data stream can be read from.
      * @param out The raw file descriptor that normal command messages should be written to.
      * @param err The raw file descriptor that command error messages should be written to.
@@ -1408,10 +1423,15 @@
                 } else {
                     Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
                 }
+                onUnhandledException(code, flags, e);
             } else {
                 // Clear the parcel before writing the exception.
                 reply.setDataSize(0);
                 reply.setDataPosition(0);
+                // The writeException below won't do anything useful if this is the case.
+                if (Parcel.getExceptionCode(e) == 0) {
+                    onUnhandledException(code, flags, e);
+                }
                 reply.writeException(e);
             }
             res = true;
diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java
index 0776cf4..e2a72dd 100644
--- a/core/java/android/os/IpcDataCache.java
+++ b/core/java/android/os/IpcDataCache.java
@@ -48,6 +48,20 @@
  * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
  * but doesn't hold a lock across data fetches on query misses.
  *
+ * Clients should be aware of the following commonly-seen issues:
+ * <ul>
+ *
+ * <li>Client calls will not go through the cache before the first invalidation signal is
+ * received. Therefore, servers should signal an invalidation as soon as they have data to offer to
+ * clients.
+ *
+ * <li>Cache invalidation is restricted to well-known processes, which means that test code cannot
+ * invalidate a cache.  {@link #disableForTestMode()} and {@link #testPropertyName} must be used in
+ * test processes that attempt cache invalidation.  See
+ * {@link PropertyInvalidatedCacheTest#testBasicCache()} for an example.
+ *
+ * </ul>
+ *
  * The intended use case is caching frequently-read, seldom-changed information normally retrieved
  * across interprocess communication. Imagine that you've written a user birthday information
  * daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over
@@ -136,20 +150,20 @@
  * string is permitted.  The third parameters is the name of the API being cached; this, too, can
  * any value.  The fourth is the name of the cache.  The cache is usually named after th API.
  * Some things you must know about the three strings:
- * <list>
- * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
+ * <ul>
+ * <li> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
  * Usually, the SELinux rules permit a process to write a system property (and therefore
  * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}.  This means that
  * although the cache can be constructed with any module string, whatever string is chosen must be
  * consistent with the SELinux configuration.
- * <ul> The API name can be any string of alphanumeric characters.  All caches with the same API
+ * <li> The API name can be any string of alphanumeric characters.  All caches with the same API
  * are invalidated at the same time.  If a server supports several caches and all are invalidated
  * in common, then it is most efficient to assign the same API string to every cache.
- * <ul> The cache name can be any string.  In debug output, the name is used to distiguish between
+ * <li> The cache name can be any string.  In debug output, the name is used to distiguish between
  * caches with the same API name.  The cache name is also used when disabling caches in the
  * current process.  So, invalidation is based on the module+api but disabling (which is generally
  * a once-per-process operation) is based on the cache name.
- * </list>
+ * </ul>
  *
  * User birthdays do occasionally change, so we have to modify the server to invalidate this
  * cache when necessary. That invalidation code looks like this:
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index 4c9a02c..dfc591b 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -28,6 +28,8 @@
 import android.text.format.DateUtils;
 import android.util.Slog;
 
+import com.android.internal.os.ApplicationSharedMemory;
+
 import dalvik.annotation.optimization.CriticalNative;
 
 import java.time.Clock;
@@ -323,74 +325,74 @@
     }
 
     /**
-     * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized
-     * using a remote network source outside the device.
-     * <p>
-     * While the time returned by {@link System#currentTimeMillis()} can be
-     * adjusted by the user, the time returned by this method cannot be adjusted
-     * by the user.
-     * <p>
-     * This performs no blocking network operations and returns values based on
-     * a recent successful synchronization event; it will either return a valid
-     * time or throw.
-     * <p>
-     * Note that synchronization may occur using an insecure network protocol,
-     * so the returned time should not be used for security purposes.
-     * The device may resynchronize with the same or different network source
-     * at any time. Due to network delays, variations between servers, or local
-     * (client side) clock drift, the accuracy of the returned times cannot be
-     * guaranteed. In extreme cases, consecutive calls to {@link
-     * #currentNetworkTimeMillis(ITimeDetectorService)} could return times that
-     * are out of order.
+     * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized using a remote
+     * network source outside the device.
+     *
+     * <p>While the time returned by {@link System#currentTimeMillis()} can be adjusted by the user,
+     * the time returned by this method cannot be adjusted by the user.
+     *
+     * <p>This performs no blocking network operations and returns values based on a recent
+     * successful synchronization event; it will either return a valid time or throw.
+     *
+     * <p>Note that synchronization may occur using an insecure network protocol, so the returned
+     * time should not be used for security purposes. The device may resynchronize with the same or
+     * different network source at any time. Due to network delays, variations between servers, or
+     * local (client side) clock drift, the accuracy of the returned times cannot be guaranteed. In
+     * extreme cases, consecutive calls to {@link #currentNetworkTimeMillis()} could return times
+     * that are out of order.
      *
      * @throws DateTimeException when no network time can be provided.
      * @hide
      */
     public static long currentNetworkTimeMillis() {
-        ITimeDetectorService timeDetectorService = getITimeDetectorService();
-        if (timeDetectorService == null) {
-            throw new RuntimeException(new DeadSystemException());
-        }
+        if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                && Flags.networkTimeUsesSharedMemory()) {
+            final long latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis =
+                    ApplicationSharedMemory.getInstance()
+                            .getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+            return latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis + elapsedRealtime();
+        } else {
+            ITimeDetectorService timeDetectorService = getITimeDetectorService();
+            if (timeDetectorService == null) {
+                throw new RuntimeException(new DeadSystemException());
+            }
 
-        UnixEpochTime time;
-        try {
-            time = timeDetectorService.latestNetworkTime();
-        } catch (ParcelableException e) {
-            e.maybeRethrow(DateTimeException.class);
-            throw new RuntimeException(e);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        if (time == null) {
-            // This is not expected.
-            throw new DateTimeException("Network based time is not available.");
-        }
+            UnixEpochTime time;
+            try {
+                time = timeDetectorService.latestNetworkTime();
+            } catch (ParcelableException e) {
+                e.maybeRethrow(DateTimeException.class);
+                throw new RuntimeException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            if (time == null) {
+                // This is not expected.
+                throw new DateTimeException("Network based time is not available.");
+            }
 
-        long currentMillis = elapsedRealtime();
-        long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
-        return time.getUnixEpochTimeMillis() + deltaMs;
+            long currentMillis = elapsedRealtime();
+            long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
+            return time.getUnixEpochTimeMillis() + deltaMs;
+        }
     }
 
-   /**
-     * Returns a {@link Clock} that starts at January 1, 1970 00:00:00.0 UTC,
-     * synchronized using a remote network source outside the device.
-     * <p>
-     * While the time returned by {@link System#currentTimeMillis()} can be
-     * adjusted by the user, the time returned by this method cannot be adjusted
-     * by the user.
-     * <p>
-     * This performs no blocking network operations and returns values based on
-     * a recent successful synchronization event; it will either return a valid
-     * time or throw.
-     * <p>
-     * Note that synchronization may occur using an insecure network protocol,
-     * so the returned time should not be used for security purposes.
-     * The device may resynchronize with the same or different network source
-     * at any time. Due to network delays, variations between servers, or local
-     * (client side) clock drift, the accuracy of the returned times cannot be
-     * guaranteed. In extreme cases, consecutive calls to {@link
-     * Clock#millis()} on the returned {@link Clock} could return times that are
-     * out of order.
+    /**
+     * Returns a {@link Clock} that starts at January 1, 1970 00:00:00.0 UTC, synchronized using a
+     * remote network source outside the device.
+     *
+     * <p>While the time returned by {@link System#currentTimeMillis()} can be adjusted by the user,
+     * the time returned by this method cannot be adjusted by the user.
+     *
+     * <p>This performs no blocking network operations and returns values based on a recent
+     * successful synchronization event; it will either return a valid time or throw.
+     *
+     * <p>Note that synchronization may occur using an insecure network protocol, so the returned
+     * time should not be used for security purposes. The device may resynchronize with the same or
+     * different network source at any time. Due to network delays, variations between servers, or
+     * local (client side) clock drift, the accuracy of the returned times cannot be guaranteed. In
+     * extreme cases, consecutive calls to {@link Clock#millis()} on the returned {@link Clock}
+     * could return times that are out of order.
      *
      * @throws DateTimeException when no network time can be provided.
      */
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
index e53873b..89b727c 100644
--- a/core/java/android/os/SystemProperties.java
+++ b/core/java/android/os/SystemProperties.java
@@ -21,13 +21,10 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodRedirect;
-import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.MutableInt;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.ravenwood.RavenwoodEnvironment;
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
@@ -40,8 +37,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Predicate;
 
 /**
  * Gives access to the system properties store.  The system properties
@@ -58,7 +53,6 @@
  */
 @SystemApi
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("SystemProperties_host")
 public class SystemProperties {
     private static final String TAG = "SystemProperties";
     private static final boolean TRACK_KEY_ACCESS = false;
@@ -76,7 +70,7 @@
 
     @UnsupportedAppUsage
     @GuardedBy("sChangeCallbacks")
-    static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
+    private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
 
     @GuardedBy("sRoReads")
     private static final HashMap<String, MutableInt> sRoReads =
@@ -102,19 +96,6 @@
         }
     }
 
-    /** @hide */
-    @RavenwoodRedirect
-    public static void init$ravenwood(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
-        throw RavenwoodEnvironment.notSupportedOnDevice();
-    }
-
-    /** @hide */
-    @RavenwoodRedirect
-    public static void reset$ravenwood() {
-        throw RavenwoodEnvironment.notSupportedOnDevice();
-    }
-
     // The one-argument version of native_get used to be a regular native function. Nowadays,
     // we use the two-argument form of native_get all the time, but we can't just delete the
     // one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation
@@ -126,46 +107,34 @@
 
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    @RavenwoodRedirect
     private static native String native_get(String key, String def);
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    @RavenwoodRedirect
     private static native int native_get_int(String key, int def);
     @FastNative
     @UnsupportedAppUsage
-    @RavenwoodRedirect
     private static native long native_get_long(String key, long def);
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    @RavenwoodRedirect
     private static native boolean native_get_boolean(String key, boolean def);
 
     @FastNative
-    @RavenwoodRedirect
     private static native long native_find(String name);
     @FastNative
-    @RavenwoodRedirect
     private static native String native_get(long handle);
     @CriticalNative
-    @RavenwoodRedirect
     private static native int native_get_int(long handle, int def);
     @CriticalNative
-    @RavenwoodRedirect
     private static native long native_get_long(long handle, long def);
     @CriticalNative
-    @RavenwoodRedirect
     private static native boolean native_get_boolean(long handle, boolean def);
 
     // _NOT_ FastNative: native_set performs IPC and can block
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    @RavenwoodRedirect
     private static native void native_set(String key, String def);
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-    @RavenwoodRedirect
     private static native void native_add_change_callback();
-    @RavenwoodRedirect
     private static native void native_report_sysprop_change();
 
     /**
@@ -301,7 +270,7 @@
     }
 
     @SuppressWarnings("unused")  // Called from native code.
-    static void callChangeCallbacks() {
+    private static void callChangeCallbacks() {
         ArrayList<Runnable> callbacks = null;
         synchronized (sChangeCallbacks) {
             //Log.i("foo", "Calling " + sChangeCallbacks.size() + " change callbacks!");
@@ -327,6 +296,16 @@
     }
 
     /**
+     * Clear all callback changes.
+     * @hide
+     */
+    public static void clearChangeCallbacksForTest() {
+        synchronized (sChangeCallbacks) {
+            sChangeCallbacks.clear();
+        }
+    }
+
+    /**
      * Notifies listeners that a system property has changed
      * @hide
      */
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index cfbf528..a5697fb 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -16,6 +16,8 @@
 
 package android.os;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -138,14 +140,14 @@
             Log.w(TAG, "Failed to vibrate; no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
         try {
             mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason,
                     mToken);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to vibrate.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -155,14 +157,14 @@
             Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
         try {
             mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant,
                     reason, flags, privFlags);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -174,15 +176,14 @@
                             + " no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR,
-                "performHapticFeedbackForInputDevice, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
         try {
             mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName,
                     constant, inputDeviceId, inputSource, reason, flags, privFlags);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback for input device.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index f670601..a1bfe39 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -224,3 +224,11 @@
      is_exported: true
      bug: "366598445"
 }
+
+flag {
+    name: "network_time_uses_shared_memory"
+    namespace: "system_performance"
+    description: "SystemClock.currentNetworkTimeMillis() reads network time offset from shared memory"
+    bug: "361329788"
+    is_exported: true
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b8a8be1..d82af55 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10750,6 +10750,16 @@
                 "lock_screen_show_only_unseen_notifications";
 
         /**
+         * Indicates whether to minimalize the number of notifications to show on the lockscreen.
+         * <p>
+         * Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM =
+                "lock_screen_notification_minimalism";
+
+        /**
          * Indicates whether snooze options should be shown on notifications
          * <p>
          * Type: int (0 for false, 1 for true)
diff --git a/core/java/android/ranging/mock/RangingFrameworkInitializer.java b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
new file mode 100644
index 0000000..540f519
--- /dev/null
+++ b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ranging;
+
+/**
+* Mock RangingFrameworkInitializer.
+*
+* @hide
+*/
+
+// TODO(b/331206299): Remove this after RANGING_STACK_ENABLED is ramped up to next.
+public final class RangingFrameworkInitializer {
+    private RangingFrameworkInitializer() {}
+    /**
+     * @hide
+     */
+    public static void registerServiceWrappers() {
+        // No-op.
+    }
+}
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index c38ee08..325d274 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -10,3 +10,4 @@
 per-file FileIntegrityManager.java = file:platform/system/security:/fsverity/OWNERS
 per-file IFileIntegrityService.aidl = file:platform/system/security:/fsverity/OWNERS
 per-file *.aconfig = victorhsieh@google.com,eranm@google.com
+per-file *responsible_apis_flags.aconfig = haok@google.com
\ No newline at end of file
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 45e9def..5457bbe 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -24,6 +24,17 @@
 }
 
 flag {
+  name: "asm_reintroduce_grace_period"
+  namespace: "responsible_apis"
+  description: "Allow launches within the grace period for ASM apps"
+  bug: "367702727"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+
+flag {
     name: "content_uri_permission_apis"
     is_exported: true
     namespace: "responsible_apis"
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 88da8eb..48d7cf7 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -18,6 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -90,10 +91,11 @@
             = "android.service.notification.NotificationAssistantService";
 
     /**
-     * Activity Action: Show notification assistant detail setting page in NAS app.
+     * Activity Action: Show notification assistant detail setting page in the NAS app.
      * <p>
-     * In some cases, a matching Activity may not exist, so ensure you
-     * safeguard against this.
+     * To be implemented by the NAS to offer users additional customization of intelligence
+     * features. If the action is not implemented, the OS will not provide a link to it in the
+     * Settings UI.
      * <p>
      * Input: Nothing.
      * <p>
@@ -103,6 +105,30 @@
     public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS =
             "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
 
+    /**
+     * Activity Action: Open notification assistant feedback page in the NAS app.
+     * <p>
+     * If the NAS does not implement this page, the OS will not show any feedback calls to action in
+     * the UI.
+     * <p>
+     * Input: {@link #EXTRA_NOTIFICATION_KEY}, the {@link StatusBarNotification#getKey()} of the
+     * notification the user wants to file feedback for.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS =
+            "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+
+    /**
+     * A string extra containing the key of the notification that the user triggered feedback for.
+     *
+     * Extra for {@link #ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS}.
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+    public static final String EXTRA_NOTIFICATION_KEY
+            = "android.service.notification.extra.NOTIFICATION_KEY";
 
     /**
      * Data type: int, the feedback rating score provided by user. The score can be any integer
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 66776ce..ac208b5 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2064,7 +2064,11 @@
         if (mAttachInfo.mThreadedRenderer == null) return;
         if (mAttachInfo.mThreadedRenderer.setForceDark(determineForceDarkType())) {
             // TODO: Don't require regenerating all display lists to apply this setting
-            invalidateWorld(mView);
+            if (forceInvertColor()) {
+                destroyAndInvalidate();
+            } else {
+                invalidateWorld(mView);
+            }
         }
     }
 
@@ -11911,15 +11915,23 @@
         public void onHighTextContrastStateChanged(boolean enabled) {
             ThreadedRenderer.setHighContrastText(enabled);
 
-            // Destroy Displaylists so they can be recreated with high contrast recordings
-            destroyHardwareResources();
-
-            // Schedule redraw, which will rerecord + redraw all text
-            invalidate();
+            destroyAndInvalidate();
         }
     }
 
     /**
+     * Destroy Displaylists so they can be recreated with new recordings, in case you are changing
+     * the way things are rendered (e.g. high contrast, force dark), then invalidate to trigger a
+     * redraw.
+     */
+    private void destroyAndInvalidate() {
+        destroyHardwareResources();
+
+        // Schedule redraw, which will rerecord + redraw all text
+        invalidate();
+    }
+
+    /**
      * This class is an interface this ViewAncestor provides to the
      * AccessibilityManagerService to the latter can interact with
      * the view hierarchy in this ViewAncestor.
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index dda3993..d5ccca9 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -157,10 +157,10 @@
             // which prevents overlap with the DisplayCutout.
             if (!attachedInParent && !floatingInScreenWindow) {
                 mTempRect.set(outParentFrame);
-                outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+                intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars);
                 frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
             }
-            outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+            intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars);
         }
 
         final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
@@ -283,6 +283,19 @@
                 + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
     }
 
+    /**
+     * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect
+     * to the side (or the corner) that the other rectangle is away from.
+     * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle
+     * is valid and contained in inOutRect if rectangles involved are valid.
+     */
+    private static void intersectOrClamp(Rect inOutRect, Rect other) {
+        inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right);
+        inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom);
+        inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left);
+        inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top);
+    }
+
     public static void extendFrameByCutout(Rect displayCutoutSafe,
             Rect displayFrame, Rect inOutFrame, Rect tempRect) {
         if (displayCutoutSafe.contains(inOutFrame)) {
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index c88048c..1f341ca 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+import static android.os.IInputConstants.POLICY_FLAG_KEY_GESTURE_TRIGGERED;
 
 import android.annotation.IntDef;
 import android.os.PowerManager;
@@ -35,6 +36,7 @@
     int FLAG_VIRTUAL = 0x00000002;
 
     int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+    int FLAG_KEY_GESTURE_TRIGGERED = POLICY_FLAG_KEY_GESTURE_TRIGGERED;
     int FLAG_INJECTED = 0x01000000;
     int FLAG_TRUSTED = 0x02000000;
     int FLAG_FILTERED = 0x04000000;
diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
index c92593f..7b6e070 100644
--- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
@@ -880,10 +880,6 @@
      * @hide
      */
     public static String typeToString(int type) {
-        if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
-            return "TYPE_WINDOW_CONTROL";
-        }
-
         switch (type) {
             case TYPE_APPLICATION: {
                 return "TYPE_APPLICATION";
@@ -903,8 +899,12 @@
             case TYPE_MAGNIFICATION_OVERLAY: {
                 return "TYPE_MAGNIFICATION_OVERLAY";
             }
-            default:
+            default: {
+                if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
+                    return "TYPE_WINDOW_CONTROL";
+                }
                 return "<UNKNOWN:" + type + ">";
+            }
         }
     }
 
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 2f649c2..1e5c6d8 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -465,13 +465,6 @@
     private static final long USE_ASYNC_SHOW_HIDE_METHOD = 352594277L; // This is a bug id.
 
     /**
-     * Version-gating is guarded by bug-fix flag.
-     */
-    private static final boolean ASYNC_SHOW_HIDE_METHOD_ENABLED =
-            !Flags.compatchangeForZerojankproxy()
-                || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
-
-    /**
      * If {@code true}, avoid calling the
      * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService}
      * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus}
@@ -614,6 +607,15 @@
     @UnsupportedAppUsage
     Rect mCursorRect = new Rect();
 
+    /**
+     * Version-gating is guarded by bug-fix flag.
+     */
+    // Note: this is non-static so that it only gets initialized once CompatChanges has
+    // access to the correct application context.
+    private final boolean mAsyncShowHideMethodEnabled =
+            !Flags.compatchangeForZerojankproxy()
+                    || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
+
     /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */
     @GuardedBy("mH")
     private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache;
@@ -2419,7 +2421,7 @@
                         mCurRootView.getLastClickToolType(),
                         resultReceiver,
                         reason,
-                        ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2463,7 +2465,7 @@
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
                     reason,
-                    ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -2572,7 +2574,7 @@
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
-                        statsToken, flags, resultReceiver, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        statsToken, flags, resultReceiver, reason, mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2615,7 +2617,7 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(),
-                    statsToken, flags, null, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    statsToken, flags, null, reason, mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -3392,7 +3394,7 @@
                         servedInputConnection == null ? null
                                 : servedInputConnection.asIRemoteAccessibilityInputConnection(),
                         view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
-                        mImeDispatcher, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mImeDispatcher, mAsyncShowHideMethodEnabled);
             } else {
                 res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                         startInputReason, mClient, windowGainingFocus, startInputFlags,
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index cc5e583..fbc30ed 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -287,6 +287,13 @@
 }
 
 flag {
+    name: "enable_fully_immersive_in_desktop"
+    namespace: "lse_desktop_experience"
+    description: "Enabled the fully immersive experience from desktop"
+    bug: "359523924"
+}
+
+flag {
     name: "enable_display_focus_in_shell_transitions"
     namespace: "lse_desktop_experience"
     description: "Creates a shell transition when display focus switches."
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 21eefef..ccaaf63 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -70,6 +70,17 @@
 }
 
 flag {
+  name: "common_surface_animator"
+  namespace: "windowing_frontend"
+  description: "A reusable surface animator for default transition"
+  bug: "326331384"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "reduce_keyguard_transitions"
   namespace: "windowing_frontend"
   description: "Avoid setting keyguard transitions ready unless there are no other changes"
diff --git a/core/java/com/android/internal/os/ApplicationSharedMemory.java b/core/java/com/android/internal/os/ApplicationSharedMemory.java
new file mode 100644
index 0000000..84f713e
--- /dev/null
+++ b/core/java/com/android/internal/os/ApplicationSharedMemory.java
@@ -0,0 +1,296 @@
+/*
+ * 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.internal.os;
+
+import android.annotation.NonNull;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.time.DateTimeException;
+
+/**
+ * This class is used to create and access a shared memory region.
+ *
+ * <p>The intended use case is that memory is shared between system processes and application
+ * processes such that it's readable to all apps and writable only to system processes.
+ *
+ * <p>This shared memory region can be used as an alternative to Binder IPC for driving
+ * communication between system processes and application processes at a lower latency and higher
+ * throughput than Binder IPC can provide, under circumstances where the additional features of
+ * Binder IPC are not required.
+ *
+ * <p>Unlike Binder IPC, shared memory doesn't support synchronous transactions and associated
+ * ordering guarantees, client identity (and therefore caller permission checking), and access
+ * auditing. Therefore it's not a suitable alternative to Binder IPC for most use cases.
+ *
+ * <p>Additionally, because the intended use case is to make this shared memory region readable to
+ * all apps, it's not suitable for sharing sensitive data.
+ *
+ * @see {@link ApplicationSharedMemoryTestRule} for unit testing support.
+ * @hide
+ */
+public class ApplicationSharedMemory implements AutoCloseable {
+
+    // LINT.IfChange(invalid_network_time)
+    public static final long INVALID_NETWORK_TIME = -1;
+    // LINT.ThenChange(frameworks/base/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp:invalid_network_time)
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "ApplicationSharedMemory";
+
+    @VisibleForTesting public static ApplicationSharedMemory sInstance;
+
+    /** Get the process-global instance. */
+    public static ApplicationSharedMemory getInstance() {
+        ApplicationSharedMemory instance = sInstance;
+        if (instance == null) {
+            throw new IllegalStateException("ApplicationSharedMemory not initialized");
+        }
+        return instance;
+    }
+
+    /** Set the process-global instance. */
+    public static void setInstance(ApplicationSharedMemory instance) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "setInstance: " + instance);
+        }
+
+        if (sInstance != null) {
+            throw new IllegalStateException("ApplicationSharedMemory already initialized");
+        }
+        sInstance = instance;
+    }
+
+    /** Allocate mutable shared memory region. */
+    public static ApplicationSharedMemory create() {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "create");
+        }
+
+        int fd = nativeCreate();
+        FileDescriptor fileDescriptor = new FileDescriptor();
+        fileDescriptor.setInt$(fd);
+
+        final boolean mutable = true;
+        long ptr = nativeMap(fd, mutable);
+        nativeInit(ptr);
+
+        return new ApplicationSharedMemory(fileDescriptor, mutable, ptr);
+    }
+
+    /**
+     * Open shared memory region from a given {@link FileDescriptor}.
+     *
+     * @param fileDescriptor Handle to shared memory region.
+     * @param mutable Whether the shared memory region is mutable. If true, will be mapped as
+     *     read-write memory. If false, will be mapped as read-only memory. Passing true (mutable)
+     *     if |pfd| is a handle to read-only memory will result in undefined behavior.
+     */
+    public static ApplicationSharedMemory fromFileDescriptor(
+            @NonNull FileDescriptor fileDescriptor, boolean mutable) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "fromFileDescriptor: " + fileDescriptor + " mutable: " + mutable);
+        }
+
+        long ptr = nativeMap(fileDescriptor.getInt$(), mutable);
+        return new ApplicationSharedMemory(fileDescriptor, mutable, ptr);
+    }
+
+    /**
+     * Allocate read-write shared memory region.
+     *
+     * @return File descriptor of the shared memory region.
+     */
+    private static native int nativeCreate();
+
+    /**
+     * Map the shared memory region.
+     *
+     * @param fd File descriptor of the shared memory region.
+     * @param isMutable Whether the shared memory region is mutable. If true, will be mapped as
+     *     read-write memory. If false, will be mapped as read-only memory.
+     * @return Pointer to the mapped shared memory region.
+     */
+    private static native long nativeMap(int fd, boolean isMutable);
+
+    /**
+     * Initialize read-write shared memory region.
+     *
+     * @param Pointer to the mapped shared memory region.
+     */
+    private static native void nativeInit(long ptr);
+
+    /**
+     * Unmap the shared memory region.
+     *
+     * @param ptr Pointer to the mapped shared memory region.
+     */
+    private static native void nativeUnmap(long ptr);
+
+    /**
+     * If true, this object owns the read-write instance of the shared memory region. If false, this
+     * object can only read.
+     */
+    private final boolean mMutable;
+
+    /**
+     * Handle to the shared memory region. This can be send to other processes over Binder calls or
+     * Intent extras. Recipients can use this handle to obtain read-only access to the shared memory
+     * region.
+     */
+    private FileDescriptor mFileDescriptor;
+
+    /** Native pointer to the mapped shared memory region. */
+    private volatile long mPtr;
+
+    ApplicationSharedMemory(@NonNull FileDescriptor fileDescriptor, boolean mutable, long ptr) {
+        mFileDescriptor = fileDescriptor;
+        mMutable = mutable;
+        mPtr = ptr;
+    }
+
+    /**
+     * Returns the file descriptor of the shared memory region.
+     *
+     * <p>This file descriptor retains the mutability properties of this object instance, and can be
+     * sent over Binder IPC or Intent extras to another process to allow the remote process to map
+     * the same shared memory region with the same access rights.
+     *
+     * @throws IllegalStateException if the file descriptor is closed.
+     */
+    public FileDescriptor getFileDescriptor() {
+        checkFileOpen();
+        return mFileDescriptor;
+    }
+
+    /**
+     * Returns a read-only file descriptor of the shared memory region. This object can be sent over
+     * Binder IPC or Intent extras to another process to allow the remote process to map the same
+     * shared memory region with read-only access.
+     *
+     * @return a read-only handle to the shared memory region.
+     * @throws IllegalStateException if the file descriptor is closed.
+     */
+    public FileDescriptor getReadOnlyFileDescriptor() throws IOException {
+        checkFileOpen();
+        FileDescriptor readOnlyFileDescriptor = new FileDescriptor();
+        int readOnlyFd = nativeDupAsReadOnly(mFileDescriptor.getInt$());
+        readOnlyFileDescriptor.setInt$(readOnlyFd);
+        return readOnlyFileDescriptor;
+    }
+
+    /** Return a read-only duplicate of the file descriptor. */
+    private static native int nativeDupAsReadOnly(int fd);
+
+    /** Set the latest network Unix Epoch minus realtime millis. */
+    public void setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(long offset) {
+        checkMutable();
+        nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(mPtr, offset);
+    }
+
+    /** Clear the latest network Unix Epoch minus realtime millis. */
+    public void clearLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis() {
+        checkMutable();
+        nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+                mPtr, INVALID_NETWORK_TIME);
+    }
+
+    @CriticalNative
+    private static native void nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+            long ptr, long offset);
+
+    /**
+     * Get the latest network Unix Epoch minus realtime millis.
+     *
+     * @throws DateTimeException when no network time can be provided.
+     */
+    public long getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()
+            throws DateTimeException {
+        checkMapped();
+        long offset = nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(mPtr);
+        if (offset == INVALID_NETWORK_TIME) {
+            throw new DateTimeException("No network time available");
+        }
+        return offset;
+    }
+
+    @CriticalNative
+    public static native long nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+            long ptr);
+
+    /**
+     * Close the associated file descriptor.
+     *
+     * <p>This method is safe to call if you never intend to pass the file descriptor to another
+     * process, whether via {@link #getFileDescriptor()} or {@link #getReadOnlyFileDescriptor()}.
+     * After calling this method, subsequent calls to {@link #getFileDescriptor()} or {@link
+     * #getReadOnlyFileDescriptor()} will throw an {@link IllegalStateException}.
+     */
+    public void closeFileDescriptor() {
+        if (mFileDescriptor != null) {
+            IoUtils.closeQuietly(mFileDescriptor);
+            mFileDescriptor = null;
+        }
+    }
+
+    public void close() {
+        if (mPtr != 0) {
+            nativeUnmap(mPtr);
+            mPtr = 0;
+        }
+
+        if (mFileDescriptor != null) {
+            IoUtils.closeQuietly(mFileDescriptor);
+            mFileDescriptor = null;
+        }
+    }
+
+    private void checkFileOpen() {
+        if (mFileDescriptor == null) {
+            throw new IllegalStateException("File descriptor is closed");
+        }
+    }
+
+    /**
+     * Check that the shared memory region is mapped.
+     *
+     * @throws IllegalStateException if the shared memory region is not mapped.
+     */
+    private void checkMapped() {
+        if (mPtr == 0) {
+            throw new IllegalStateException("Instance is closed");
+        }
+    }
+
+    /**
+     * Check that the shared memory region is mapped and mutable.
+     *
+     * @throws IllegalStateException if the shared memory region is not mapped or not mutable.
+     */
+    private void checkMutable() {
+        checkMapped();
+        if (!mMutable) {
+            throw new IllegalStateException("Not mutable");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/os/OWNERS b/core/java/com/android/internal/os/OWNERS
index 391d257..ffd4499 100644
--- a/core/java/com/android/internal/os/OWNERS
+++ b/core/java/com/android/internal/os/OWNERS
@@ -15,3 +15,6 @@
 # ANRs
 # Bug component : 158088 = per-file TimeoutRecord.java
 per-file TimeoutRecord.java = file:/PERFORMANCE_OWNERS
+
+# ApplicationSharedMemory
+per-file *ApplicationSharedMemory* = file:/PERFORMANCE_OWNERS
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index c7117e9..07df248 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -27,4 +27,12 @@
     description: "If the debug store is enabled."
     bug: "314735374"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "application_shared_memory_enabled"
+    namespace: "system_performance"
+    description: "Whether ApplicationSharedMemory is enabled."
+    bug: "365575551"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 12d3264..032ac42 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -3025,6 +3025,7 @@
     @Override
     public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) {
         this.splitCodePaths = splitCodePaths;
+        this.mSplits = null; // reset for paths changed
         if (splitCodePaths != null) {
             int size = splitCodePaths.length;
             for (int index = 0; index < size; index++) {
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index c8b7def..702e5e2 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -33,6 +33,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
+import android.view.Display;
 import android.view.WindowManager;
 
 import java.util.Objects;
@@ -53,11 +54,12 @@
     private final Bitmap mBitmap;
     private final Rect mBoundsInScreen;
     private final Insets mInsets;
+    private final int mDisplayId;
 
     private ScreenshotRequest(
             @WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
             ComponentName topComponent, int taskId, int userId,
-            Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+            Bitmap bitmap, Rect boundsInScreen, Insets insets, int displayId) {
         mType = type;
         mSource = source;
         mTopComponent = topComponent;
@@ -66,6 +68,7 @@
         mBitmap = bitmap;
         mBoundsInScreen = boundsInScreen;
         mInsets = insets;
+        mDisplayId = displayId;
     }
 
     ScreenshotRequest(Parcel in) {
@@ -77,6 +80,7 @@
         mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
         mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
         mInsets = in.readTypedObject(Insets.CREATOR);
+        mDisplayId = in.readInt();
     }
 
     @WindowManager.ScreenshotType
@@ -113,6 +117,10 @@
         return mTopComponent;
     }
 
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -128,6 +136,7 @@
         dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
         dest.writeTypedObject(mBoundsInScreen, 0);
         dest.writeTypedObject(mInsets, 0);
+        dest.writeInt(mDisplayId);
     }
 
     @NonNull
@@ -161,6 +170,7 @@
         private int mTaskId = INVALID_TASK_ID;
         private int mUserId = USER_NULL;
         private ComponentName mTopComponent;
+        private int mDisplayId = Display.INVALID_DISPLAY;
 
         /**
          * Begin building a ScreenshotRequest.
@@ -193,7 +203,7 @@
             }
 
             return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
-                    mBoundsInScreen, mInsets);
+                    mBoundsInScreen, mInsets, mDisplayId);
         }
 
         /**
@@ -255,6 +265,16 @@
             mInsets = insets;
             return this;
         }
+
+        /**
+         * Set the display ID for this request.
+         *
+         * @param displayId see {@link Display}
+         */
+        public Builder setDisplayId(int displayId) {
+            mDisplayId = displayId;
+            return this;
+        }
     }
 
     /**
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 9a4ff8f..9797d96 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -258,6 +258,7 @@
                 "com_android_internal_content_om_OverlayConfig.cpp",
                 "com_android_internal_content_om_OverlayManagerImpl.cpp",
                 "com_android_internal_net_NetworkUtilsInternal.cpp",
+                "com_android_internal_os_ApplicationSharedMemory.cpp",
                 "com_android_internal_os_ClassLoaderFactory.cpp",
                 "com_android_internal_os_DebugStore.cpp",
                 "com_android_internal_os_FuseAppLoop.cpp",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 03b5143a..70a80b2 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -201,6 +201,7 @@
 extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env);
 extern int register_com_android_internal_content_om_OverlayManagerImpl(JNIEnv* env);
 extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
+extern int register_com_android_internal_os_ApplicationSharedMemory(JNIEnv *env);
 extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env);
 extern int register_com_android_internal_os_DebugStore(JNIEnv* env);
 extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
@@ -1516,6 +1517,7 @@
 }
 
 static const RegJNIRec gRegJNI[] = {
+        REG_JNI(register_com_android_internal_os_ApplicationSharedMemory),
         REG_JNI(register_com_android_internal_os_RuntimeInit),
         REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
         REG_JNI(register_android_os_SystemClock),
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index c0fe098..af10623 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -116,3 +116,6 @@
 
 # IF Tools
 per-file android_tracing_Perfetto* = file:platform/development:/tools/winscope/OWNERS
+
+# ApplicationSharedMemory
+per-file *ApplicationSharedMemory* = file:/PERFORMANCE_OWNERS
diff --git a/core/jni/android_os_SystemProperties.cpp b/core/jni/android_os_SystemProperties.cpp
index 88e6fa3..e99d890 100644
--- a/core/jni/android_os_SystemProperties.cpp
+++ b/core/jni/android_os_SystemProperties.cpp
@@ -34,8 +34,6 @@
 
 #if defined(__BIONIC__)
 # include <sys/system_properties.h>
-#else
-struct prop_info;
 #endif
 
 namespace android {
@@ -46,7 +44,6 @@
 template<typename Functor>
 void ReadProperty(const prop_info* prop, Functor&& functor)
 {
-#if defined(__BIONIC__)
     auto thunk = [](void* cookie,
                     const char* /*name*/,
                     const char* value,
@@ -54,9 +51,6 @@
         std::forward<Functor>(*static_cast<Functor*>(cookie))(value);
     };
     __system_property_read_callback(prop, thunk, &functor);
-#else
-    LOG(FATAL) << "fast property access supported only on device";
-#endif
 }
 
 template<typename Functor>
@@ -66,16 +60,11 @@
     if (!key.c_str()) {
         return;
     }
-#if defined(__BIONIC__)
     const prop_info* prop = __system_property_find(key.c_str());
     if (!prop) {
         return;
     }
     ReadProperty(prop, std::forward<Functor>(functor));
-#else
-    std::forward<Functor>(functor)(
-        android::base::GetProperty(key.c_str(), "").c_str());
-#endif
 }
 
 jstring SystemProperties_getSS(JNIEnv* env, jclass clazz, jstring keyJ,
@@ -132,17 +121,12 @@
 
 jlong SystemProperties_find(JNIEnv* env, jclass, jstring keyJ)
 {
-#if defined(__BIONIC__)
     ScopedUtfChars key(env, keyJ);
     if (!key.c_str()) {
         return 0;
     }
     const prop_info* prop = __system_property_find(key.c_str());
     return reinterpret_cast<jlong>(prop);
-#else
-    LOG(FATAL) << "fast property access supported only on device";
-    __builtin_unreachable();  // Silence warning
-#endif
 }
 
 jstring SystemProperties_getH(JNIEnv* env, jclass clazz, jlong propJ)
@@ -198,11 +182,7 @@
     // request" failures).
     errno = 0;
     bool success;
-#if defined(__BIONIC__)
     success = !__system_property_set(key.c_str(), value_c_str);
-#else
-    success = android::base::SetProperty(key.c_str(), value_c_str);
-#endif
     if (!success) {
         if (errno != 0) {
             jniThrowExceptionFmt(env, "java/lang/RuntimeException",
diff --git a/core/jni/android_util_XmlBlock.cpp b/core/jni/android_util_XmlBlock.cpp
index 5a444bb..c364451 100644
--- a/core/jni/android_util_XmlBlock.cpp
+++ b/core/jni/android_util_XmlBlock.cpp
@@ -83,7 +83,7 @@
         return 0;
     }
 
-    ResXMLParser* st = new ResXMLParser(*osb);
+    ResXMLParser* st = new(std::nothrow) ResXMLParser(*osb);
     if (st == NULL) {
         jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
         return 0;
diff --git a/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp
new file mode 100644
index 0000000..453e539
--- /dev/null
+++ b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+// See: ApplicationSharedMemory.md
+
+#include <cutils/ashmem.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <nativehelper/JNIHelp.h>
+#include <string.h>
+#include <sys/mman.h>
+
+#include <atomic>
+#include <cstddef>
+#include <new>
+
+#include "core_jni_helpers.h"
+
+namespace {
+
+// Atomics should be safe to use across processes if they are lock free.
+static_assert(std::atomic<int64_t>::is_always_lock_free == true,
+              "atomic<int64_t> is not always lock free");
+
+// This is the data structure that is shared between processes.
+//
+// Tips for extending:
+// - Atomics are safe for cross-process use as they are lock free, if they are accessed as
+//   individual values.
+// - Consider multi-ABI systems, e.g. devices that support launching both 64-bit and 32-bit
+//   app processes. Use fixed-size types (e.g. `int64_t`) to ensure that the data structure is
+//   the same size across all ABIs. Avoid implicit assumptions about struct packing/padding.
+class alignas(8) SharedMemory { // Ensure that `sizeof(SharedMemory)` is the same across 32-bit and
+                                // 64-bit systems.
+private:
+    volatile std::atomic<int64_t> latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis;
+
+    // LINT.IfChange(invalid_network_time)
+    static constexpr int64_t INVALID_NETWORK_TIME = -1;
+    // LINT.ThenChange(frameworks/base/core/java/com/android/internal/os/ApplicationSharedMemory.java:invalid_network_time)
+
+public:
+    // Default constructor sets initial values
+    SharedMemory()
+          : latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(INVALID_NETWORK_TIME) {}
+
+    int64_t getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis() const {
+        return latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis;
+    }
+
+    void setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(int64_t offset) {
+        latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis = offset;
+    }
+};
+
+// Update the expected value when modifying the members of SharedMemory.
+// The goal of this assertion is to ensure that the data structure is the same size across 32-bit
+// and 64-bit systems.
+static_assert(sizeof(SharedMemory) == 8, "Unexpected SharedMemory size");
+
+static jint nativeCreate(JNIEnv* env, jclass) {
+    // Create anonymous shared memory region
+    int fd = ashmem_create_region("ApplicationSharedMemory", sizeof(SharedMemory));
+    if (fd < 0) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to create ashmem: %s",
+                             strerror(errno));
+    }
+    return fd;
+}
+
+static jlong nativeMap(JNIEnv* env, jclass, jint fd, jboolean isMutable) {
+    void* ptr = mmap(nullptr, sizeof(SharedMemory), isMutable ? PROT_READ | PROT_WRITE : PROT_READ,
+                     MAP_SHARED, fd, 0);
+    if (ptr == MAP_FAILED) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to mmap shared memory: %s",
+                             strerror(errno));
+    }
+
+    return reinterpret_cast<jlong>(ptr);
+}
+
+static void nativeInit(JNIEnv* env, jclass, jlong ptr) {
+    new (reinterpret_cast<SharedMemory*>(ptr)) SharedMemory();
+}
+
+static void nativeUnmap(JNIEnv* env, jclass, jlong ptr) {
+    if (munmap(reinterpret_cast<void*>(ptr), sizeof(SharedMemory)) == -1) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException",
+                             "Failed to munmap shared memory: %s", strerror(errno));
+    }
+}
+
+static jint nativeDupAsReadOnly(JNIEnv* env, jclass, jint fd) {
+    // Duplicate file descriptor
+    fd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
+    if (fd < 0) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to dup fd: %s",
+                             strerror(errno));
+    }
+
+    // Set new file descriptor to read-only
+    if (ashmem_set_prot_region(fd, PROT_READ)) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException",
+                             "Failed to ashmem_set_prot_region: %s", strerror(errno));
+    }
+
+    return fd;
+}
+
+static void nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(jlong ptr,
+                                                                                 jlong offset) {
+    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
+    sharedMemory->setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(offset);
+}
+
+static jlong nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(jlong ptr) {
+    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
+    return sharedMemory->getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeCreate", "()I", (void*)nativeCreate},
+        {"nativeMap", "(IZ)J", (void*)nativeMap},
+        {"nativeInit", "(J)V", (void*)nativeInit},
+        {"nativeUnmap", "(J)V", (void*)nativeUnmap},
+        {"nativeDupAsReadOnly", "(I)I", (void*)nativeDupAsReadOnly},
+        {"nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(JJ)V",
+         (void*)nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis},
+        {"nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(J)J",
+         (void*)nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis},
+};
+
+} // anonymous namespace
+
+namespace android {
+
+static const char kApplicationSharedMemoryClassName[] =
+        "com/android/internal/os/ApplicationSharedMemory";
+static jclass gApplicationSharedMemoryClass;
+
+int register_com_android_internal_os_ApplicationSharedMemory(JNIEnv* env) {
+    gApplicationSharedMemoryClass =
+            MakeGlobalRefOrDie(env, FindClassOrDie(env, kApplicationSharedMemoryClassName));
+    RegisterMethodsOrDie(env, "com/android/internal/os/ApplicationSharedMemory", gMethods,
+                         NELEM(gMethods));
+    return JNI_OK;
+}
+
+} // namespace android
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index e7f0560..258832e 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -157,10 +157,8 @@
     option (.android.msg_privacy).dest = DEST_AUTOMATIC;
     repeated int32 vibrator_ids = 1;
     optional VibrationProto current_vibration = 2;
-    optional bool is_vibrating = 3;
     optional int32 is_vibrator_controller_registered = 27;
     optional VibrationProto current_external_vibration = 4;
-    optional bool vibrator_under_external_control = 5;
     optional bool low_power_mode = 6;
     optional bool vibrate_on = 24;
     reserved 25; // prev keyboard_vibration_on
@@ -183,4 +181,6 @@
     repeated VibrationProto previous_vibrations = 16;
     repeated VibrationParamProto previous_vibration_params = 28;
     reserved 17; // prev previous_external_vibrations
+    reserved 3; // prev is_vibrating, check current_vibration instead
+    reserved 5; // prev vibrator_under_external_control, check current_external_vibration instead
 }
\ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 07efad8..92c3906 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4457,6 +4457,11 @@
     <!-- Bytes that the PinnerService will pin for WebView -->
     <integer name="config_pinnerWebviewPinBytes">0</integer>
 
+    <!-- Maximum memory that PinnerService will pin for apps expressed
+         as a percentage of total device memory [0,100].
+         Example: 10, means 10% of total memory will be the maximum pinned memory -->
+    <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer>
+
     <!-- Number of days preloaded file cache should be preserved on a device before it can be
          deleted -->
     <integer name="config_keepPreloadsMinDays">7</integer>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index f397ef2..6683dc0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -808,6 +808,12 @@
          This is bigger than displayed because listeners can use it for other displays
          e.g. wearables. -->
     <dimen name="notification_person_icon_max_size">144dp</dimen>
+    <!-- The size of the progress bar icon -->
+    <dimen name="notification_progress_icon_size">20dp</dimen>
+    <!-- The size of the progress tracker width -->
+    <dimen name="notification_progress_tracker_width">40dp</dimen>
+    <!-- The size of the progress tracker height -->
+    <dimen name="notification_progress_tracker_height">20dp</dimen>
 
     <!-- The maximum size of the small notification icon on low memory devices. -->
     <dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 06b36b8..5f40a6c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3467,6 +3467,7 @@
   <java-symbol type="integer" name="config_pinnerHomePinBytes" />
   <java-symbol type="bool" name="config_pinnerAssistantApp" />
   <java-symbol type="integer" name="config_pinnerWebviewPinBytes" />
+  <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" />
 
   <java-symbol type="string" name="config_doubleTouchGestureEnableFile" />
 
@@ -3854,6 +3855,9 @@
   <java-symbol type="dimen" name="notification_custom_view_max_image_height"/>
   <java-symbol type="dimen" name="notification_custom_view_max_image_width"/>
   <java-symbol type="dimen" name="notification_person_icon_max_size" />
+  <java-symbol type="dimen" name="notification_progress_icon_size" />
+  <java-symbol type="dimen" name="notification_progress_tracker_width" />
+  <java-symbol type="dimen" name="notification_progress_tracker_height" />
 
   <java-symbol type="dimen" name="notification_small_icon_size_low_ram"/>
   <java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index e9ce712..9821d43 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -25,6 +25,7 @@
         "BinderProxyCountingTestApp/src/**/*.java",
         "BinderProxyCountingTestService/src/**/*.java",
         "BinderDeathRecipientHelperApp/src/**/*.java",
+        "AppThatCallsBinderMethods/src/**/*.kt",
     ],
     visibility: ["//visibility:private"],
 }
@@ -144,6 +145,7 @@
         ":BinderProxyCountingTestApp",
         ":BinderProxyCountingTestService",
         ":AppThatUsesAppOps",
+        ":AppThatCallsBinderMethods",
     ],
 }
 
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index b1f1e2c..05ab783 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -26,6 +26,7 @@
         <option name="test-file-name" value="BinderProxyCountingTestApp.apk" />
         <option name="test-file-name" value="BinderProxyCountingTestService.apk" />
         <option name="test-file-name" value="AppThatUsesAppOps.apk" />
+        <option name="test-file-name" value="AppThatCallsBinderMethods.apk" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/Android.bp b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
new file mode 100644
index 0000000..dcc0d4f
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
@@ -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.
+
+android_test_helper_app {
+    name: "AppThatCallsBinderMethods",
+    srcs: ["src/**/*.kt"],
+    platform_apis: true,
+    static_libs: ["coretests-aidl"],
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
new file mode 100644
index 0000000..b2f6d78
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.coretests.methodcallerhelperapp">
+    <application>
+        <receiver android:name="com.android.frameworks.coretests.methodcallerhelperapp.CallMethodsReceiver"
+                  android:exported="true"/>
+    </application>
+
+</manifest>
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
new file mode 100644
index 0000000..638cc3b
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.frameworks.coretests.methodcallerhelperapp
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+
+/**
+ * Receiver used to call methods when a binder is received
+ * {@link android.os.BinderUncaughtExceptionHandlerTest}.
+ */
+class CallMethodsReceiver : BroadcastReceiver() {
+    private val TAG = "CallMethodsReceiver"
+
+    override fun onReceive(context: Context, intent: Intent) {
+        try {
+            when (intent.getAction()) {
+                ACTION_CALL_METHOD -> intent.getExtras()!!.let {
+                    Log.i(TAG, "Received ACTION_CALL_METHOD with extras: $it")
+                    val iface = it.getBinder(EXTRA_BINDER)!!.let(ITestInterface.Stub::asInterface)!!
+                    val name = it.getString(EXTRA_METHOD_NAME)!!
+                    try {
+                        when (name) {
+                            "foo" -> iface.foo(5)
+                            "onewayFoo" -> iface.onewayFoo(5)
+                            "bar" -> iface.bar(5)
+                            else -> Log.e(TAG, "Unknown method name")
+                        }
+                    } catch (e: Exception) {
+                        // Exceptions expected
+                    }
+                }
+                else -> Log.e(TAG, "Unknown action " + intent.getAction())
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Exception: ", e)
+        }
+    }
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
new file mode 100644
index 0000000..37c6268
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.frameworks.coretests.methodcallerhelperapp
+
+const val PACKAGE_NAME = "com.android.frameworks.coretests.methodcallerhelperapp"
+const val RECEIVER_NAME = "CallMethodsReceiver"
+const val ACTION_CALL_METHOD = PACKAGE_NAME + ".ACTION_CALL_METHOD"
+const val EXTRA_METHOD_NAME = PACKAGE_NAME + ".EXTRA_METHOD_NAME"
+const val EXTRA_BINDER = PACKAGE_NAME + ".EXTRA_BINDER"
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
new file mode 100644
index 0000000..ffcf178
--- /dev/null
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
@@ -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.frameworks.coretests.aidl;
+
+/**
+ * Just an interface with a oneway, void and non-oneway method.
+ */
+interface ITestInterface {
+    // Method order matters, since we verify transaction codes
+    int foo(int a);
+    oneway void onewayFoo(int a);
+    void bar(int a);
+}
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0f73df9..edcea24 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -97,7 +97,6 @@
 import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.Pair;
-import android.util.Slog;
 import android.widget.RemoteViews;
 
 import androidx.test.InstrumentationRegistry;
@@ -118,6 +117,7 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -2114,6 +2114,300 @@
         assertThat(n.getWhen()).isEqualTo(9);
     }
 
+    @Test
+    public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() {
+        final List<Class<? extends Notification.Style>> platformStyleClasses = List.of(
+                Notification.BigTextStyle.class, Notification.BigPictureStyle.class,
+                Notification.MessagingStyle.class, Notification.CallStyle.class,
+                Notification.InboxStyle.class, Notification.MediaStyle.class,
+                Notification.DecoratedCustomViewStyle.class,
+                Notification.DecoratedMediaCustomViewStyle.class
+        );
+
+        for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) {
+            assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName()))
+                    .isEqualTo(platformStyleClass);
+        }
+    }
+
+    @Test
+    public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() {
+        assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName()))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_richOngoingEnabled_platformClass() {
+        assertThat(
+                Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+                .isEqualTo(Notification.ProgressStyle.class);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_richOngoingDisabled_notPlatformClass() {
+        assertThat(
+                Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onSegmentChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.RED)));
+
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.BLUE)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.RED)));
+
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.BLUE)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onStartIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onEndIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onProgressChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgress(20));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgress(21));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgress(20));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgress(21));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setStyledByProgress(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setStyledByProgress(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setStyledByProgress(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setStyledByProgress(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onProgressStepChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressStep(new Notification.ProgressStyle.Step(10)));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressStep(new Notification.ProgressStyle.Step(10)));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onTrackerIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .setProgressTrackerIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgressTrackerIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onIndeterminateChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_default100() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_nooSegments_returnsDefault() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle.setProgressSegments(Collections.emptyList());
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_returnsSumOfSegmentLength() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle
+                .addProgressSegment(new Notification.ProgressStyle.Segment(10))
+                .addProgressSegment(new Notification.ProgressStyle.Segment(20));
+
+        assertThat(progressStyle.getProgressMax()).isEqualTo(30);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle
+                .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE))
+                .addProgressSegment(new Notification.ProgressStyle.Segment(10));
+
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_indeterminate_defaultValueFalse() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+        assertThat(progressStyle1.isProgressIndeterminate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_styledByProgress_defaultValueTrue() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+        assertThat(progressStyle1.isStyledByProgress()).isTrue();
+    }
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
@@ -2214,4 +2508,11 @@
                 new Intent(action).setPackage(mContext.getPackageName()),
                 PendingIntent.FLAG_MUTABLE);
     }
+
+    private static class NotAPlatformStyle extends Notification.Style {
+        @Override
+        public boolean areNotificationsVisiblyDifferent(Notification.Style other) {
+            return false;
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
new file mode 100644
index 0000000..791c209
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os
+
+import android.content.Intent
+import android.platform.test.annotations.DisabledOnRavenwood
+import android.platform.test.annotations.Presubmit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+import com.android.frameworks.coretests.methodcallerhelperapp.*
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.intThat
+import org.mockito.Mockito.after
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.quality.Strictness.STRICT_STUBS
+
+private const val TIMEOUT_DURATION_MS = 2000L
+private const val FALSE_NEG_DURATION_MS = 500L
+private const val FLAG_ONEWAY = 1
+// From ITestInterface.Stub class, these values are package private
+private const val TRANSACTION_foo = 1
+private const val TRANSACTION_onewayFoo = 2
+private const val TRANSACTION_bar = 3
+
+/** Tests functionality of {@link android.os.Binder.onUnhandledException}. */
+@DisabledOnRavenwood(reason = "multi-app")
+@Presubmit
+@RunWith(AndroidJUnit4::class)
+class BinderUncaughtExceptionHandlerTest {
+
+    val mContext = InstrumentationRegistry.getInstrumentation().getTargetContext()
+
+    @Rule @JvmField val rule = MockitoJUnit.rule().strictness(STRICT_STUBS)
+
+    @Spy var mInterfaceImpl: ITestImpl = ITestImpl()
+
+    // This subclass is needed for visibility issues (via protected), since the method we are
+    // verifying lives on the boot classpath, it is not enough to be in the same package.
+    open class ITestImpl : ITestInterface.Stub() {
+        override fun onUnhandledException(code: Int, flags: Int, e: Exception?) =
+            onUnhandledExceptionVisible(code, flags, e)
+
+        public open fun onUnhandledExceptionVisible(code: Int, flags: Int, e: Exception?) {}
+
+        @Throws(RemoteException::class)
+        override open fun foo(x: Int): Int = throw UnsupportedOperationException()
+
+        @Throws(RemoteException::class)
+        override open fun onewayFoo(x: Int): Unit = throw UnsupportedOperationException()
+
+        @Throws(RemoteException::class)
+        override open fun bar(x: Int): Unit = throw UnsupportedOperationException()
+    }
+
+    class OnewayMatcher(private val isOneway: Boolean) : ArgumentMatcher<Int> {
+        override fun matches(argument: Int?) =
+            (argument!! and FLAG_ONEWAY) == if (isOneway) 1 else 0
+
+        override fun toString() = "Expected oneway: $isOneway"
+    }
+
+    @Test
+    fun testRegularMethod_ifThrowsRuntimeException_HandlerCalled() {
+        val myException = RuntimeException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+        dispatchActionCall("foo")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_foo),
+                /* flags= */ intThat(OnewayMatcher(false)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testRegularMethod_ifThrowsRemoteException_HandlerCalled() {
+        val myException = RemoteException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+        dispatchActionCall("foo")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_foo),
+                /* flags= */ intThat(OnewayMatcher(false)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testRegularMethod_ifThrowsSecurityException_HandlerNotCalled() {
+        val myException = SecurityException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+        dispatchActionCall("foo")
+
+        // No unexpected calls
+        verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+            .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testVoidMethod_ifThrowsRuntimeException_HandlerCalled() {
+        val myException = RuntimeException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+        dispatchActionCall("bar")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_bar),
+                /* flags= */ intThat(OnewayMatcher(false)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testVoidMethod_ifThrowsRemoteException_HandlerCalled() {
+        val myException = RemoteException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+        dispatchActionCall("bar")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_bar),
+                /* flags= */ intThat(OnewayMatcher(false)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testVoidMethod_ifThrowsSecurityException_HandlerNotCalled() {
+        val myException = SecurityException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+        dispatchActionCall("bar")
+
+        // No unexpected calls
+        verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+            .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testOnewayMethod_ifThrowsRuntimeException_HandlerCalled() {
+        val myException = RuntimeException("Test exception")
+        doThrow(myException).doNothing().`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+        dispatchActionCall("onewayFoo")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+                /* flags= */ intThat(OnewayMatcher(true)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun testOnewayMethod_ifThrowsRemoteException_HandlerCalled() {
+        val myException = RemoteException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+        dispatchActionCall("onewayFoo")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+                /* flags= */ intThat(OnewayMatcher(true)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    // All exceptions are uncaught for oneway
+    @Test
+    fun testOnewayMethod_ifThrowsSecurityException_HandlerCalled() {
+        val myException = SecurityException("Test exception")
+        doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+        dispatchActionCall("onewayFoo")
+
+        verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+            .onUnhandledExceptionVisible(
+                /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+                /* flags= */ intThat(OnewayMatcher(true)),
+                /* exception= */ eq(myException),
+            )
+        // No unexpected calls
+        verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+    }
+
+    private fun dispatchActionCall(methodName: String) =
+        Intent(ACTION_CALL_METHOD).apply {
+            putExtras(
+                Bundle().apply {
+                    putBinder(EXTRA_BINDER, mInterfaceImpl as IBinder)
+                    putString(EXTRA_METHOD_NAME, methodName)
+                }
+            )
+            setClassName(PACKAGE_NAME, CallMethodsReceiver::class.java.getName())
+        }.let { mContext.sendBroadcast(it) }
+}
diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
index 5cac98d..d4693e6 100644
--- a/core/tests/coretests/src/android/view/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -413,4 +413,19 @@
         assertInsetByTopBottom(0, 0, mFrames.parentFrame);
         assertInsetByTopBottom(0, 0, mFrames.frame);
     }
+
+    @Test
+    public void windowBoundsOutsideDisplayCutoutSafe() {
+        addDisplayCutout();
+        mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+        mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0);
+        computeFrames();
+
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.displayFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.parentFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.frame);
+    }
 }
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index 005538a..d9e90fa 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -29,6 +29,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -78,6 +79,16 @@
     }
 
     @Test
+    public void testNegatedDisabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool5)).isTrue();
+    }
+
+    @Test
+    public void testNegatedEnabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool6)).isTrue();
+    }
+
+    @Test
     public void testFlagEnabledDifferentCompilationUnit() {
         assertThat(mResources.getBoolean(R.bool.bool3)).isTrue();
     }
@@ -94,6 +105,26 @@
     }
 
     @Test
+    public void testDirectoryEnabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool8)).isTrue();
+    }
+
+    @Test
+    public void testDirectoryDisabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool7)).isTrue();
+    }
+
+    @Test
+    public void testDirectoryNegatedEnabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool9)).isTrue();
+    }
+
+    @Test
+    public void testDirectoryNegatedDisabledFlag() {
+        assertThat(mResources.getBoolean(R.bool.bool10)).isTrue();
+    }
+
+    @Test
     public void testLayoutWithDisabledElements() {
         LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout1, null);
         assertThat(ll).isNotNull();
@@ -102,6 +133,24 @@
         assertThat((View) ll.findViewById(R.id.text2)).isNotNull();
     }
 
+    @Test
+    public void testEnabledFlagLayoutOverrides() {
+        LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout3, null);
+        assertThat(ll).isNotNull();
+        assertThat((View) ll.findViewById(R.id.text1)).isNotNull();
+        assertThat(((TextView) ll.findViewById(R.id.text1)).getText()).isEqualTo("foobar");
+    }
+
+    @Test(expected = Resources.NotFoundException.class)
+    public void testDisabledLayout() {
+        getLayoutInflater().inflate(R.layout.layout2, null);
+    }
+
+    @Test(expected = Resources.NotFoundException.class)
+    public void testDisabledDrawable() {
+        mResources.getDrawable(R.drawable.removedpng);
+    }
+
     private LayoutInflater getLayoutInflater() {
         ContextWrapper c = new ContextWrapper(mContext) {
             private LayoutInflater mInflater;
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 89acbc7..0ce403e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -35,6 +35,7 @@
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
 import android.os.Parcel;
+import android.view.Display;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -64,10 +65,12 @@
         assertNull("Bitmap was expected to be null", out.getBitmap());
         assertNull("Bounds were expected to be null", out.getBoundsInScreen());
         assertEquals(Insets.NONE, out.getInsets());
+        assertEquals(Display.INVALID_DISPLAY, out.getDisplayId());
     }
 
     @Test
     public void testProvidedScreenshot() {
+        int displayId = 5;
         Bitmap bitmap = makeHardwareBitmap(50, 50);
         ScreenshotRequest in =
                 new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
@@ -77,6 +80,7 @@
                         .setBitmap(bitmap)
                         .setBoundsOnScreen(new Rect(10, 10, 60, 60))
                         .setInsets(Insets.of(2, 3, 4, 5))
+                        .setDisplayId(displayId)
                         .build();
 
         Parcel parcel = Parcel.obtain();
@@ -92,6 +96,7 @@
         assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
         assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
         assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+        assertEquals(displayId, out.getDisplayId());
     }
 
     @Test
diff --git a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
index d98120f..75aca1b 100644
--- a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
+++ b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
@@ -23,11 +23,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.platform.test.ravenwood.RavenwoodRule;
+import android.platform.test.ravenwood.RavenwoodConfig;
 
 import androidx.test.filters.SmallTest;
 
-import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Objects;
@@ -35,16 +34,18 @@
 import java.util.concurrent.TimeUnit;
 
 public class SystemPropertiesTest {
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
-            .setSystemPropertyMutable(KEY, null)
-            .setSystemPropertyMutable(UNSET_KEY, null)
-            .setSystemPropertyMutable(PERSIST_KEY, null)
-            .build();
-
     private static final String KEY = "sys.testkey";
     private static final String UNSET_KEY = "Aiw7woh6ie4toh7W";
     private static final String PERSIST_KEY = "persist.sys.testkey";
+    private static final String NONEXIST_KEY = "doesnotexist_2341431";
+
+    @RavenwoodConfig.Config
+    public static final RavenwoodConfig mRavenwood = new RavenwoodConfig.Builder()
+            .setSystemPropertyMutable(KEY, null)
+            .setSystemPropertyMutable(UNSET_KEY, null)
+            .setSystemPropertyMutable(PERSIST_KEY, null)
+            .setSystemPropertyImmutable(NONEXIST_KEY, null)
+            .build();
 
     @Test
     @SmallTest
@@ -117,7 +118,7 @@
     @SmallTest
     public void testHandle() throws Exception {
         String value;
-        SystemProperties.Handle handle = SystemProperties.find("doesnotexist_2341431");
+        SystemProperties.Handle handle = SystemProperties.find(NONEXIST_KEY);
         assertNull(handle);
         SystemProperties.set(KEY, "abc");
         handle = SystemProperties.find(KEY);
diff --git a/graphics/java/android/graphics/PathIterator.java b/graphics/java/android/graphics/PathIterator.java
index 48b29f4..d7caabf 100644
--- a/graphics/java/android/graphics/PathIterator.java
+++ b/graphics/java/android/graphics/PathIterator.java
@@ -44,6 +44,8 @@
     private final Path mPath;
     private final int mPathGenerationId;
     private static final int POINT_ARRAY_SIZE = 8;
+    private static final boolean IS_DALVIK = "dalvik".equalsIgnoreCase(
+            System.getProperty("java.vm.name"));
 
     private static final NativeAllocationRegistry sRegistry =
             NativeAllocationRegistry.createMalloced(
@@ -80,9 +82,14 @@
         mPath = path;
         mNativeIterator = nCreate(mPath.mNativePath);
         mPathGenerationId = mPath.getGenerationId();
-        final VMRuntime runtime = VMRuntime.getRuntime();
-        mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
-        mPointsAddress = runtime.addressOf(mPointsArray);
+        if (IS_DALVIK) {
+            final VMRuntime runtime = VMRuntime.getRuntime();
+            mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
+            mPointsAddress = runtime.addressOf(mPointsArray);
+        } else {
+            mPointsArray = new float[POINT_ARRAY_SIZE];
+            mPointsAddress = 0;
+        }
         sRegistry.registerNativeAllocation(this, mNativeIterator);
     }
 
@@ -177,7 +184,8 @@
             throw new ConcurrentModificationException(
                     "Iterator cannot be used on modified Path");
         }
-        @Verb int verb = nNext(mNativeIterator, mPointsAddress);
+        @Verb int verb = IS_DALVIK
+            ? nNext(mNativeIterator, mPointsAddress) : nNextHost(mNativeIterator, mPointsArray);
         if (verb == VERB_DONE) {
             mDone = true;
         }
@@ -287,6 +295,9 @@
     private static native long nCreate(long nativePath);
     private static native long nGetFinalizer();
 
+    /* nNextHost should be used for host runtimes, e.g. LayoutLib */
+    private static native int nNextHost(long nativeIterator, float[] points);
+
     // ------------------ Critical JNI ------------------------
 
     @CriticalNative
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 37f0067..0896138 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,12 +16,11 @@
 
 package androidx.window.common;
 
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 
 import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
 import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
 
-import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.devicestate.DeviceState;
 import android.hardware.devicestate.DeviceStateManager;
@@ -31,16 +30,23 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
+import androidx.annotation.BinderThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.common.layout.DisplayFoldFeatureCommon;
 
 import com.android.internal.R;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -55,13 +61,6 @@
     private static final boolean DEBUG = false;
 
     /**
-     * Emulated device state
-     * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
-     * {@link CommonFoldingFeature.State} map.
-     */
-    private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
-
-    /**
      * Device state received via
      * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
      * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
@@ -71,23 +70,40 @@
      * "rear display". Concurrent mode for example is activated via public API and can be active in
      * both the "open" and "half folded" device states.
      */
-    private DeviceState mCurrentDeviceState = new DeviceState(
-            new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
-                    "INVALID").build());
+    // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup.
+    private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
 
-    private List<DeviceState> mSupportedStates;
+    /**
+     * Lock to synchronize access to {@link #mCurrentDeviceState}.
+     *
+     * <p>This lock is used to ensure thread-safety when accessing and modifying the
+     * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if
+     * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if
+     * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and
+     * ensure data consistency.
+     */
+    private final Object mCurrentDeviceStateLock = new Object();
 
     @NonNull
     private final RawFoldingFeatureProducer mRawFoldSupplier;
 
-    private final boolean mIsHalfOpenedSupported;
+    @NonNull
+    private final DeviceStateMapper mDeviceStateMapper;
 
-    private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+    @VisibleForTesting
+    final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+        // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
+        // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
+        @SuppressWarnings("GuardedBy")
+        @BinderThread // When Flags.wlinfoOncreate() is enabled.
+        @MainThread // When Flags.wlinfoOncreate() is disabled.
         @Override
         public void onDeviceStateChanged(@NonNull DeviceState state) {
-            mCurrentDeviceState = state;
-            mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
-                    .this::notifyFoldingFeatureChange);
+            synchronized (mCurrentDeviceStateLock) {
+                mCurrentDeviceState = state;
+                mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
+                        ::notifyFoldingFeatureChangeLocked);
+            }
         }
     };
 
@@ -95,41 +111,14 @@
             @NonNull RawFoldingFeatureProducer rawFoldSupplier,
             @NonNull DeviceStateManager deviceStateManager) {
         mRawFoldSupplier = rawFoldSupplier;
-        String[] deviceStatePosturePairs = context.getResources()
-                .getStringArray(R.array.config_device_state_postures);
-        mSupportedStates = deviceStateManager.getSupportedDeviceStates();
-        boolean isHalfOpenedSupported = false;
-        for (String deviceStatePosturePair : deviceStatePosturePairs) {
-            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
-            if (deviceStatePostureMapping.length != 2) {
-                if (DEBUG) {
-                    Log.e(TAG, "Malformed device state posture pair: "
-                            + deviceStatePosturePair);
-                }
-                continue;
-            }
+        mDeviceStateMapper =
+                new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
 
-            int deviceState;
-            int posture;
-            try {
-                deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
-                posture = Integer.parseInt(deviceStatePostureMapping[1]);
-            } catch (NumberFormatException e) {
-                if (DEBUG) {
-                    Log.e(TAG, "Failed to parse device state or posture: "
-                                    + deviceStatePosturePair,
-                            e);
-                }
-                continue;
-            }
-            isHalfOpenedSupported = isHalfOpenedSupported
-                    || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
-            mDeviceStateToPostureMap.put(deviceState, posture);
-        }
-        mIsHalfOpenedSupported = isHalfOpenedSupported;
-        if (mDeviceStateToPostureMap.size() > 0) {
+        if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
+            final Executor executor =
+                    Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor();
             Objects.requireNonNull(deviceStateManager)
-                    .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+                    .registerCallback(executor, mDeviceStateCallback);
         }
     }
 
@@ -137,50 +126,51 @@
      * Add a callback to mCallbacks if there is no device state. This callback will be run
      * once a device state is set. Otherwise,run the callback immediately.
      */
-    private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
-            String displayFeaturesString) {
-        if (isCurrentStateValid()) {
-            callback.accept(calculateFoldingFeature(displayFeaturesString));
+    private void runCallbackWhenValidState(@NonNull DeviceState state,
+            @NonNull Consumer<List<CommonFoldingFeature>> callback,
+            @NonNull String displayFeaturesString) {
+        if (mDeviceStateMapper.isDeviceStateValid(state)) {
+            callback.accept(calculateFoldingFeature(state, displayFeaturesString));
         } else {
             // This callback will be added to mCallbacks and removed once it runs once.
-            AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+            final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
                     new AcceptOnceConsumer<>(this, callback);
             addDataChangedCallback(singleRunCallback);
         }
     }
 
-    /**
-     * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
-     * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
-     * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
-     * Returns a boolean value of whether the device state is valid.
-     */
-    private boolean isCurrentStateValid() {
-        // If the device state is not found in the map, indexOfKey returns a negative number.
-        return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
-    }
-
+    // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of
+    // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations.
+    @SuppressWarnings("GuardedBy")
     @Override
     protected void onListenersChanged() {
         super.onListenersChanged();
-        if (hasListeners()) {
-            mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
-        } else {
-            mCurrentDeviceState = new DeviceState(
-                    new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
-                            "INVALID").build());
-            mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+        synchronized (mCurrentDeviceStateLock) {
+            if (hasListeners()) {
+                mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+            } else {
+                mCurrentDeviceState = INVALID_DEVICE_STATE;
+                mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+            }
+        }
+    }
+
+    @NonNull
+    private DeviceState getCurrentDeviceState() {
+        synchronized (mCurrentDeviceStateLock) {
+            return mCurrentDeviceState;
         }
     }
 
     @NonNull
     @Override
     public Optional<List<CommonFoldingFeature>> getCurrentData() {
-        Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
-        if (!isCurrentStateValid()) {
+        final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+        final DeviceState state = getCurrentDeviceState();
+        if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) {
             return Optional.empty();
         } else {
-            return displayFeaturesString.map(this::calculateFoldingFeature);
+            return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get()));
         }
     }
 
@@ -191,7 +181,7 @@
      */
     @NonNull
     public List<CommonFoldingFeature> getFoldsWithUnknownState() {
-        Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
+        final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
 
         if (optionalFoldingFeatureString.isPresent()) {
             return CommonFoldingFeature.parseListFromString(
@@ -201,7 +191,6 @@
         return Collections.emptyList();
     }
 
-
     /**
      * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
      * {@link DeviceStateManagerFoldingFeatureProducer}.
@@ -218,16 +207,16 @@
         return foldFeatures;
     }
 
-
     /**
      * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
      */
     public boolean isHalfOpenedSupported() {
-        return mIsHalfOpenedSupported;
+        return mDeviceStateMapper.mIsHalfOpenedSupported;
     }
 
     /**
      * Adds the data to the storeFeaturesConsumer when the data is ready.
+     *
      * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
      */
     @Override
@@ -236,38 +225,123 @@
             if (TextUtils.isEmpty(displayFeaturesString)) {
                 storeFeaturesConsumer.accept(new ArrayList<>());
             } else {
-                runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+                final DeviceState state = getCurrentDeviceState();
+                runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString);
             }
         });
     }
 
-    private void notifyFoldingFeatureChange(String displayFeaturesString) {
-        if (!isCurrentStateValid()) {
+    @GuardedBy("mCurrentDeviceStateLock")
+    private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) {
+        final DeviceState state = mCurrentDeviceState;
+        if (!mDeviceStateMapper.isDeviceStateValid(state)) {
             return;
         }
         if (TextUtils.isEmpty(displayFeaturesString)) {
             notifyDataChanged(new ArrayList<>());
         } else {
-            notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
+            notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString));
         }
     }
 
-    private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
-        return parseListFromString(displayFeaturesString, currentHingeState());
-    }
-
-    @CommonFoldingFeature.State
-    private int currentHingeState() {
+    @NonNull
+    private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState,
+            @NonNull String displayFeaturesString) {
         @CommonFoldingFeature.State
-        int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
-                COMMON_STATE_UNKNOWN);
+        final int hingeState = mDeviceStateMapper.getHingeState(deviceState);
+        return parseListFromString(displayFeaturesString, hingeState);
+    }
 
-        if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
-            posture = mDeviceStateToPostureMap.get(
-                    DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
-                            mSupportedStates), COMMON_STATE_UNKNOWN);
+    /**
+     * Internal class to map device states to corresponding postures.
+     *
+     * <p>This class encapsulates the logic for mapping device states to postures. The mapping is
+     * immutable after initialization to ensure thread safety.
+     */
+    private static class DeviceStateMapper {
+        /**
+         * Emulated device state
+         * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
+         * {@link CommonFoldingFeature.State} map.
+         *
+         * <p>This map must be immutable after initialization to ensure thread safety, as it may be
+         * accessed from multiple threads. Modifications should only occur during object
+         * construction.
+         */
+        private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+        /**
+         * The list of device states that are supported.
+         *
+         * <p>This list must be immutable after initialization to ensure thread safety.
+         */
+        @NonNull
+        private final List<DeviceState> mSupportedStates;
+
+        final boolean mIsHalfOpenedSupported;
+
+        DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) {
+            mSupportedStates = supportedStates;
+
+            final String[] deviceStatePosturePairs = context.getResources()
+                    .getStringArray(R.array.config_device_state_postures);
+            boolean isHalfOpenedSupported = false;
+            for (String deviceStatePosturePair : deviceStatePosturePairs) {
+                final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+                if (deviceStatePostureMapping.length != 2) {
+                    if (DEBUG) {
+                        Log.e(TAG, "Malformed device state posture pair: "
+                                + deviceStatePosturePair);
+                    }
+                    continue;
+                }
+
+                final int deviceState;
+                final int posture;
+                try {
+                    deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+                    posture = Integer.parseInt(deviceStatePostureMapping[1]);
+                } catch (NumberFormatException e) {
+                    if (DEBUG) {
+                        Log.e(TAG, "Failed to parse device state or posture: "
+                                        + deviceStatePosturePair,
+                                e);
+                    }
+                    continue;
+                }
+                isHalfOpenedSupported = isHalfOpenedSupported
+                        || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+                mDeviceStateToPostureMap.put(deviceState, posture);
+            }
+            mIsHalfOpenedSupported = isHalfOpenedSupported;
         }
 
-        return posture;
+        boolean isDeviceStateToPostureMapEmpty() {
+            return mDeviceStateToPostureMap.size() == 0;
+        }
+
+        /**
+         * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap}
+         * which was initialized in the constructor of {@link DeviceStateMapper}.
+         * Returns a boolean value of whether the device state is valid.
+         */
+        boolean isDeviceStateValid(@NonNull DeviceState deviceState) {
+            // If the device state is not found in the map, indexOfKey returns a negative number.
+            return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0;
+        }
+
+        @CommonFoldingFeature.State
+        int getHingeState(@NonNull DeviceState deviceState) {
+            @CommonFoldingFeature.State
+            final int posture =
+                    mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN);
+            if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+                return posture;
+            }
+
+            final int baseStateIdentifier =
+                    DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates);
+            return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN);
+        }
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 74cce68..dcc2d93 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -377,8 +377,16 @@
 
     @Nullable
     TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
-        return getContainer(container -> container.hasAppearedActivity(activityToken)
-                || container.hasPendingAppearedActivity(activityToken));
+        // When the new activity is launched to the topmost TF because the source activity
+        // was in that TF, and the source activity is finished before resolving the new activity,
+        // we will try to see if the new activity match a rule with the split activities below.
+        // If matched, it can be reparented.
+        final TaskFragmentContainer taskFragmentContainer
+                = getContainer(container -> container.hasPendingAppearedActivity(activityToken));
+        if (taskFragmentContainer != null) {
+            return taskFragmentContainer;
+        }
+        return getContainer(container -> container.hasAppearedActivity(activityToken));
     }
 
     @Nullable
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index bd430c0..09185ee 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -29,6 +29,7 @@
 
     srcs: [
         "**/*.java",
+        "**/*.kt",
     ],
 
     static_libs: [
@@ -41,6 +42,7 @@
         "androidx.test.ext.junit",
         "flag-junit",
         "mockito-target-extended-minus-junit4",
+        "mockito-kotlin-nodeps",
         "truth",
         "testables",
         "platform-test-annotations",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
new file mode 100644
index 0000000..90887a7
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
@@ -0,0 +1,341 @@
+/*
+ * 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 androidx.window.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.hardware.devicestate.DeviceState
+import android.hardware.devicestate.DeviceStateManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.window.common.layout.CommonFoldingFeature
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE
+import androidx.window.common.layout.DisplayFoldFeatureCommon
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN
+import com.android.internal.R
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+/**
+ * Test class for [DeviceStateManagerFoldingFeatureProducer].
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
+ */
+@RunWith(AndroidJUnit4::class)
+class DeviceStateManagerFoldingFeatureProducerTest {
+    @get:Rule
+    val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    private val mMockDeviceStateManager = mock<DeviceStateManager>()
+    private val mMockResources = mock<Resources> {
+        on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES
+    }
+    private val mMockContext = mock<Context> {
+        on { resources } doReturn mMockResources
+    }
+    private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> {
+        on { currentData } doReturn Optional.of(DISPLAY_FEATURES)
+        on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+            val callback = invocation.getArgument(0) as Consumer<String>
+            callback.accept(DISPLAY_FEATURES)
+        }
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() {
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() {
+        val executorCaptor = ArgumentCaptor.forClass(Executor::class.java)
+        val runnable = mock<Runnable>()
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any())
+        executorCaptor.value.execute(runnable)
+        verify(runnable).run()
+    }
+
+    @Test
+    fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+
+        val currentData = ffp.getCurrentData()
+
+        assertThat(currentData).isPresent()
+        assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val currentData = ffp.getCurrentData()
+
+        assertThat(currentData).isEmpty()
+    }
+
+    @Test
+    fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.getFoldsWithUnknownState()
+
+        assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() {
+        mRawFoldSupplier.stub {
+            on { currentData } doReturn Optional.empty()
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.getFoldsWithUnknownState()
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() {
+        mRawFoldSupplier.stub {
+            on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE)
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.displayFeatures
+
+        assertThat(result).containsExactly(
+            DisplayFoldFeatureCommon(
+                DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN,
+                setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED),
+            ),
+        )
+    }
+
+    @Test
+    fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        assertThat(ffp.isHalfOpenedSupported).isTrue()
+    }
+
+    @Test
+    fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() {
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray()
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        assertThat(ffp.isHalfOpenedSupported).isFalse()
+    }
+
+    @Test
+    fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() {
+        mRawFoldSupplier.stub {
+            on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+                val callback = invocation.getArgument(0) as Consumer<String>
+                callback.accept("")
+            }
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer).accept(emptyList())
+    }
+
+    @Test
+    fun testGetData_validState_callsConsumerWithFoldingFeatures() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer, never()).accept(any())
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
+        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() {
+        val malformedDeviceStatePostures = arrayOf(
+            // Missing the posture.
+            "0",
+            // Empty string.
+            "",
+            // Too many elements.
+            "0:1:2",
+        )
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn
+                    malformedDeviceStatePostures
+        }
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+    }
+
+    @Test
+    fun testDeviceStateMapper_invalidNumberFormat_skipsPair() {
+        val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1")
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn
+                    invalidNumberFormatDeviceStatePostures
+        }
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+    }
+
+    companion object {
+        // Supported device states configuration.
+        private enum class SupportedDeviceStates {
+            CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT;
+
+            override fun toString() = ordinal.toString()
+
+            fun toDeviceState(): DeviceState =
+                DeviceState(DeviceState.Configuration.Builder(ordinal, name).build())
+        }
+
+        // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture.
+        private val DEVICE_STATE_POSTURES =
+            arrayOf(
+                "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES",
+                "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED",
+                "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT",
+                "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES",
+                "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE",
+            )
+        private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState()
+        private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState()
+
+        // WindowsManager Jetpack display features.
+        private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]"
+        private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened"
+        private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+            DISPLAY_FEATURES,
+            COMMON_STATE_HALF_OPENED,
+        )
+        private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+            DISPLAY_FEATURES,
+            COMMON_STATE_UNKNOWN,
+        )
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 7fab371..bc4916a 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -535,7 +535,8 @@
         // container1.
         container2.setInfo(mTransaction, mInfo);
 
-        assertTrue(container2.hasActivity(mActivity.getActivityToken()));
+        assertTrue(container1.hasActivity(mActivity.getActivityToken()));
+        assertFalse(container2.hasActivity(mActivity.getActivityToken()));
         // When the pending appeared record is removed from container1, we respect the appeared
         // record in container2.
         container1.removePendingAppearedActivity(mActivity.getActivityToken());
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 63a2880..cf0a975 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -156,6 +156,13 @@
 }
 
 flag {
+    name: "enable_flexible_two_app_split"
+    namespace: "multitasking"
+    description: "Enables only 2 app 90:10 split"
+    bug: "349828130"
+}
+
+flag {
     name: "enable_flexible_split"
     namespace: "multitasking"
     description: "Enables flexibile split feature for split screen"
diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
new file mode 100644
index 0000000..07e5ac1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?android:attr/textColorTertiary"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/system_on_tertiary_fixed"
+        android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
new file mode 100644
index 0000000..a12a746
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="30dp" />
+            <solid android:color="@android:color/system_tertiary_fixed" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
new file mode 100644
index 0000000..aadffb5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- An arrow that points towards left. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="10dp"
+    android:height="12dp"
+    android:viewportWidth="10"
+    android:viewportHeight="12">
+  <path
+      android:pathData="M2.858,4.285C1.564,5.062 1.564,6.938 2.858,7.715L10,12L10,0L2.858,4.285Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
new file mode 100644
index 0000000..e3c9a66
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- An arrow that points upwards. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="9dp"
+    android:viewportWidth="12"
+    android:viewportHeight="9">
+  <path
+      android:pathData="M7.715,1.858C6.938,0.564 5.062,0.564 4.285,1.858L0,9L12,9L7.715,1.858Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
new file mode 100644
index 0000000..a269b9e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="horizontal">
+
+    <!-- ImageView for the arrow icon, positioned horizontally at the start of the tooltip
+    container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="10dp"
+        android:layout_height="12dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
new file mode 100644
index 0000000..bdee883
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tooltip_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/desktop_windowing_education_tooltip_background"
+    android:orientation="horizontal"
+    android:padding="@dimen/desktop_windowing_education_tooltip_padding">
+
+    <ImageView
+        android:id="@+id/tooltip_icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/app_handle_education_tooltip_icon" />
+
+    <TextView
+        android:id="@+id/tooltip_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="2dp"
+        android:lineHeight="20dp"
+        android:maxWidth="150dp"
+        android:textColor="@android:color/system_on_tertiary_fixed"
+        android:textFontWeight="500"
+        android:textSize="14sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
new file mode 100644
index 0000000..c73c1da
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="vertical">
+
+    <!-- ImageView for the arrow icon, positioned vertically above the tooltip container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="12dp"
+        android:layout_height="9dp"
+        android:layout_gravity="center_horizontal"
+        android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
index 045b975..462a49c 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -99,11 +99,11 @@
 
                 </LinearLayout>
 
-                <FrameLayout
+
+                <LinearLayout
                     android:minHeight="@dimen/letterbox_restart_dialog_button_height"
-                    android:layout_width="match_parent"
+                    android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    style="?android:attr/buttonBarButtonStyle"
                     android:layout_gravity="end">
 
                     <Button
@@ -133,7 +133,7 @@
                         android:text="@string/letterbox_restart_restart"
                         android:contentDescription="@string/letterbox_restart_restart"/>
 
-                </FrameLayout>
+                </LinearLayout>
 
             </LinearLayout>
 
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3d87183..c7109f5 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -608,6 +608,9 @@
     <!-- The horizontal inset to apply to the close button's ripple drawable -->
     <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>
 
+    <!-- The padding added to all sides of windowing education tooltip -->
+    <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen>
+
     <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
     <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
     <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index bda5686..56f25da 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -219,6 +219,15 @@
          compatibility control. [CHAR LIMIT=NONE] -->
     <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string>
 
+    <!-- App handle education tooltip text for tooltip pointing to app handle -->
+    <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to windowing image button -->
+    <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to app chip -->
+    <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string>
+
     <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] -->
     <string name="letterbox_education_dialog_title">See and do more</string>
 
@@ -307,12 +316,11 @@
     <!-- Maximize menu snap buttons string. -->
     <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
     <!-- Snap resizing non-resizable string. -->
-    <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string>
+    <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string>
     <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string>
     <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
     <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string>
-
 </resources>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
new file mode 100644
index 0000000..26aae2d
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Listener to get focus-related transition callbacks.
+ */
+@ExternalThread
+public interface FocusTransitionListener {
+    /**
+     * Called when a transition changes the top, focused display.
+     */
+    void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
new file mode 100644
index 0000000..b91d5b6
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+/**
+ * Listener interface that to get focus-related transition callbacks.
+ */
+oneway interface IFocusTransitionListener {
+
+    /**
+     * Called when a transition changes the top, focused display.
+     */
+    void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 3256abf..02615a9 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -20,6 +20,7 @@
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
+import com.android.wm.shell.shared.IFocusTransitionListener;
 import com.android.wm.shell.shared.IHomeTransitionListener;
 
 /**
@@ -59,4 +60,9 @@
      */
     oneway void registerRemoteForTakeover(in TransitionFilter filter,
             in RemoteTransition remoteTransition) = 6;
+
+    /**
+     * Set listener that will receive callbacks about transitions involving focus switch.
+     */
+    oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7;
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index 6d4ab4c..2db4311 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -22,6 +22,8 @@
 
 import com.android.wm.shell.shared.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to manage remote transitions.
  */
@@ -44,4 +46,15 @@
      * Unregisters a remote transition for all operations.
      */
     default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
+
+    /**
+     * Sets listener that will receive callbacks about transitions involving focus switch.
+     */
+    default void setFocusTransitionListener(@NonNull FocusTransitionListener listener,
+            Executor executor) {}
+
+    /**
+     * Unsets listener that will receive callbacks about transitions involving focus switch.
+     */
+    default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {}
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
new file mode 100644
index 0000000..bfb6d4a
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
@@ -0,0 +1,4 @@
+jeremysim@google.com
+winsonc@google.com
+peanutbutter@google.com
+shuminghao@google.com
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 498dc8b..7f1e4a8 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -66,14 +66,54 @@
     public @interface SplitPosition {
     }
 
-    /** A snap target in the first half of the screen, where the split is roughly 30-70. */
-    public static final int SNAP_TO_30_70 = 0;
+    /**
+     * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_33_66 = 0;
 
-    /** The 50-50 snap target */
-    public static final int SNAP_TO_50_50 = 1;
+    /** A snap target for two apps, where the split is 50-50.  */
+    public static final int SNAP_TO_2_50_50 = 1;
 
-    /** A snap target in the latter half of the screen, where the split is roughly 70-30. */
-    public static final int SNAP_TO_70_30 = 2;
+    /**
+     * A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_66_33 = 2;
+
+    /**
+     * A snap target for two apps, where the split is 90-10. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_90_10 = 3;
+
+    /**
+     * A snap target for two apps, where the split is 10-90. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_10_90 = 4;
+
+    /**
+     * A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_3_33_33_33 = 5;
+
+    /**
+     * A snap target for three apps, where the split is 45-45-10. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_45_45_10 = 6;
+
+    /**
+     * A snap target for three apps, where the split is 10-45-45. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_10_45_45 = 7;
 
     /**
      * These snap targets are used for split pairs in a stable, non-transient state. They may be
@@ -81,9 +121,14 @@
      * {@link SnapPosition}.
      */
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
     })
     public @interface PersistentSnapPosition {}
 
@@ -91,9 +136,14 @@
      * Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
      */
     public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) {
-        return snapPosition == SNAP_TO_30_70
-                || snapPosition == SNAP_TO_50_50
-                || snapPosition == SNAP_TO_70_30;
+        return snapPosition == SNAP_TO_2_33_66
+                || snapPosition == SNAP_TO_2_50_50
+                || snapPosition == SNAP_TO_2_66_33
+                || snapPosition == SNAP_TO_2_90_10
+                || snapPosition == SNAP_TO_2_10_90
+                || snapPosition == SNAP_TO_3_33_33_33
+                || snapPosition == SNAP_TO_3_45_45_10
+                || snapPosition == SNAP_TO_3_10_45_45;
     }
 
     /** The divider doesn't snap to any target and is freely placeable. */
@@ -109,9 +159,14 @@
     public static final int SNAP_TO_MINIMIZE = 13;
 
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30,
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
             SNAP_TO_NONE,
             SNAP_TO_START_AND_DISMISS,
             SNAP_TO_END_AND_DISMISS,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index f7f45ae..9f100fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -19,9 +19,9 @@
 import static android.view.WindowManager.DOCKED_LEFT;
 import static android.view.WindowManager.DOCKED_RIGHT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
@@ -283,10 +283,10 @@
 
     private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
             int bottomPosition, int dividerMax) {
-        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_30_70);
+        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66);
         addMiddleTarget(isHorizontalDivision);
         maybeAddTarget(bottomPosition,
-                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_70_30);
+                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33);
     }
 
     private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
@@ -332,7 +332,7 @@
     private void addMiddleTarget(boolean isHorizontalDivision) {
         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
-        mTargets.add(new SnapTarget(position, SNAP_TO_50_50));
+        mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50));
     }
 
     private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index bec2ea5..4227a6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -123,6 +123,7 @@
 import com.android.wm.shell.taskview.TaskViewFactory;
 import com.android.wm.shell.taskview.TaskViewFactoryController;
 import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.HomeTransitionObserver;
 import com.android.wm.shell.transition.MixedTransitionHandler;
 import com.android.wm.shell.transition.Transitions;
@@ -742,14 +743,15 @@
             @ShellMainThread Handler mainHandler,
             @ShellAnimationThread ShellExecutor animExecutor,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            HomeTransitionObserver homeTransitionObserver) {
+            HomeTransitionObserver homeTransitionObserver,
+            FocusTransitionObserver focusTransitionObserver) {
         if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
             // TODO(b/238217847): Force override shell init if registration is disabled
             shellInit = new ShellInit(mainExecutor);
         }
         return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
                 pool, displayController, mainExecutor, mainHandler, animExecutor,
-                rootTaskDisplayAreaOrganizer, homeTransitionObserver);
+                rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver);
     }
 
     @WMSingleton
@@ -761,6 +763,12 @@
 
     @WMSingleton
     @Provides
+    static FocusTransitionObserver provideFocusTransitionObserver() {
+        return new FocusTransitionObserver();
+    }
+
+    @WMSingleton
+    @Provides
     static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) {
         return new TaskViewTransitions(transitions);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 759ed03..0e8c4e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -456,6 +456,7 @@
             pw.println(
                 "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}"
             )
+            pw.println("${innerPrefix}minimizedTasks=${data.minimizedTasks.toDumpString()}")
         }
     }
 
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 968f40c..afa27f9 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
@@ -43,6 +43,7 @@
 import android.view.DragEvent
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -1061,7 +1062,10 @@
                     // Check if freeform task launch during recents should be handled
                     shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
                     // Check if the closing task needs to be handled
-                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
+                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(
+                        task,
+                        request.type
+                    )
                     // Check if the top task shouldn't be allowed to enter desktop mode
                     isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task)
                     // Check if fullscreen task should be updated
@@ -1288,7 +1292,10 @@
     }
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
-    private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+    private fun handleTaskClosing(
+        task: RunningTaskInfo,
+        transitionType: Int
+    ): WindowContainerTransaction? {
         logV("handleTaskClosing")
         if (!isDesktopModeShowing(task.displayId))
             return null
@@ -1301,9 +1308,10 @@
             removeWallpaperActivity(wct)
         }
         taskRepository.addClosingTask(task.displayId, task.taskId)
-        // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
+        // If a CLOSE is triggered on a desktop task, remove the task.
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
-            taskRepository.isVisibleTask(task.taskId)
+            taskRepository.isVisibleTask(task.taskId) &&
+            transitionType == TRANSIT_CLOSE
         ) {
             wct.removeTask(task.token)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 0841628..4796c4d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -16,16 +16,19 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_TO_BACK
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
+import android.window.flags.DesktopModeFlags
+import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
-import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
@@ -64,6 +67,30 @@
     ) {
         // TODO: b/332682201 Update repository state
         updateWallpaperToken(info)
+
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+            handleBackNavigation(info)
+        }
+    }
+
+    private fun handleBackNavigation(info: TransitionInfo) {
+        // When default back navigation happens, transition type is TO_BACK and the change is
+        // TO_BACK. Mark the task going to back as minimized.
+        if (info.type == TRANSIT_TO_BACK) {
+            for (change in info.changes) {
+                val taskInfo = change.taskInfo
+                if (taskInfo == null || taskInfo.taskId == -1) {
+                    continue
+                }
+
+                if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+                    change.mode == TRANSIT_TO_BACK &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+                ) {
+                    desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+                }
+            }
+        }
     }
 
     override fun onTransitionStarting(transition: IBinder) {
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 2138acc..cbb08b8 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
@@ -1344,6 +1344,9 @@
         final SurfaceControl leash = pipChange.getLeash();
         final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds();
         final boolean isInPip = mPipTransitionState.isInPip();
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b",
+                TAG, pipChange, destBounds, isInPip);
         mSurfaceTransactionHelper
                 .crop(startTransaction, leash, destBounds)
                 .round(startTransaction, leash, isInPip)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
new file mode 100644
index 0000000..a1a9ca9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.TransactionPool;
+
+import java.util.ArrayList;
+
+public class DefaultSurfaceAnimator {
+
+    /** Builds an animator for the surface and adds it to the `animations` list. */
+    static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Animation anim, @NonNull SurfaceControl leash,
+            @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
+            @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
+            @Nullable Rect clipRect, boolean isActivity) {
+        final DefaultAnimationAdapter adapter = new DefaultAnimationAdapter(anim, leash,
+                position, clipRect, cornerRadius, isActivity);
+        buildSurfaceAnimation(animations, anim, finishCallback, pool, mainExecutor, adapter);
+    }
+
+    /** Builds an animator for the surface and adds it to the `animations` list. */
+    static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Animation anim, @NonNull Runnable finishCallback,
+            @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor,
+            @NonNull AnimationAdapter updateListener) {
+        final SurfaceControl.Transaction transaction = pool.acquire();
+        updateListener.setTransaction(transaction);
+        final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+        // Animation length is already expected to be scaled.
+        va.overrideDurationScale(1.0f);
+        va.setDuration(anim.computeDurationHint());
+        va.addUpdateListener(updateListener);
+        va.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
+            private boolean mFinished;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onFinish();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
+                if (mFinished) return;
+                mFinished = true;
+                // Apply transformation of end state in case the animation is canceled.
+                if (va.getAnimatedFraction() < 1f) {
+                    va.setCurrentFraction(1f);
+                }
+
+                pool.release(transaction);
+                mainExecutor.execute(() -> {
+                    animations.remove(va);
+                    finishCallback.run();
+                });
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                va.removeUpdateListener(updateListener);
+            }
+        });
+        animations.add(va);
+    }
+
+    /** The animation adapter for buildSurfaceAnimation. */
+    abstract static class AnimationAdapter implements ValueAnimator.AnimatorUpdateListener {
+        @NonNull final SurfaceControl mLeash;
+        @NonNull SurfaceControl.Transaction mTransaction;
+        private Choreographer mChoreographer;
+
+        AnimationAdapter(@NonNull SurfaceControl leash) {
+            mLeash = leash;
+        }
+
+        void setTransaction(@NonNull SurfaceControl.Transaction transaction) {
+            mTransaction = transaction;
+        }
+
+        @Override
+        public void onAnimationUpdate(@NonNull ValueAnimator animator) {
+            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+            // with fraction 1.
+            final long currentPlayTime = animator.getAnimatedFraction() >= 1f
+                    ? animator.getDuration()
+                    : Math.min(animator.getDuration(), animator.getCurrentPlayTime());
+            applyTransformation(animator, currentPlayTime);
+            if (mChoreographer == null) {
+                mChoreographer = Choreographer.getInstance();
+            }
+            mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+            mTransaction.apply();
+        }
+
+        abstract void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime);
+    }
+
+    private static class DefaultAnimationAdapter extends AnimationAdapter {
+        final Transformation mTransformation = new Transformation();
+        final float[] mMatrix = new float[9];
+        @NonNull final Animation mAnim;
+        @Nullable final Point mPosition;
+        @Nullable final Rect mClipRect;
+        final float mCornerRadius;
+        final boolean mIsActivity;
+
+        DefaultAnimationAdapter(@NonNull Animation anim, @NonNull SurfaceControl leash,
+                @Nullable Point position, @Nullable Rect clipRect, float cornerRadius,
+                boolean isActivity) {
+            super(leash);
+            mAnim = anim;
+            mPosition = (position != null && (position.x != 0 || position.y != 0))
+                    ? position : null;
+            mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null;
+            mCornerRadius = cornerRadius;
+            mIsActivity = isActivity;
+        }
+
+        @Override
+        void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime) {
+            final Transformation transformation = mTransformation;
+            final SurfaceControl.Transaction t = mTransaction;
+            final SurfaceControl leash = mLeash;
+            transformation.clear();
+            mAnim.getTransformation(currentPlayTime, transformation);
+            if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
+                    && mIsActivity && mAnim.getExtensionEdges() != 0) {
+                t.setEdgeExtensionEffect(leash, mAnim.getExtensionEdges());
+            }
+            if (mPosition != null) {
+                transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
+            }
+            t.setMatrix(leash, transformation.getMatrix(), mMatrix);
+            t.setAlpha(leash, transformation.getAlpha());
+
+            if (mClipRect != null) {
+                Rect clipRect = mClipRect;
+                final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
+                if (!extensionInsets.equals(Insets.NONE)) {
+                    // Clip out any overflowing edge extension.
+                    clipRect = new Rect(mClipRect);
+                    clipRect.inset(extensionInsets);
+                    t.setCrop(leash, clipRect);
+                }
+                if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) {
+                    // Rounded corner can only be applied if a crop is set.
+                    t.setCrop(leash, clipRect);
+                    t.setCornerRadius(leash, mCornerRadius);
+                }
+            }
+        }
+    }
+}
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 f40e0ba..d3bed59 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
@@ -826,6 +826,11 @@
             @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
             @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
             @Nullable Rect clipRect, boolean isActivity) {
+        if (Flags.commonSurfaceAnimator()) {
+            DefaultSurfaceAnimator.buildSurfaceAnimation(animations, anim, leash, finishCallback,
+                    pool, mainExecutor, position, cornerRadius, clipRect, isActivity);
+            return;
+        }
         final SurfaceControl.Transaction transaction = pool.acquire();
         final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
         final Transformation transformation = new Transformation();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
new file mode 100644
index 0000000..2f5059f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -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.wm.shell.transition;
+
+import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
+import static com.android.wm.shell.transition.Transitions.TransitionObserver;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * The {@link TransitionObserver} that observes for transitions involving focus switch.
+ * It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
+ * and callers within the process via {@link FocusTransitionListener}.
+ */
+public class FocusTransitionObserver implements TransitionObserver {
+    private static final String TAG = FocusTransitionObserver.class.getSimpleName();
+
+    private IFocusTransitionListener mRemoteListener;
+    private final Map<FocusTransitionListener, Executor> mLocalListeners =
+            new HashMap<>();
+
+    private int mFocusedDisplayId = INVALID_DISPLAY;
+
+    public FocusTransitionObserver() {}
+
+    @Override
+    public void onTransitionReady(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        final List<TransitionInfo.Change> changes = info.getChanges();
+        for (int i = changes.size() - 1; i >= 0; i--) {
+            final TransitionInfo.Change change = changes.get(i);
+            final RunningTaskInfo task = change.getTaskInfo();
+            if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+                if (mFocusedDisplayId != task.displayId) {
+                    mFocusedDisplayId = task.displayId;
+                    notifyFocusedDisplayChanged();
+                }
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onTransitionStarting(@NonNull IBinder transition) {}
+
+    @Override
+    public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
+
+    @Override
+    public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the Shell, within the host process.
+     *
+     */
+    public void setLocalFocusTransitionListener(FocusTransitionListener listener,
+            Executor executor) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mLocalListeners.put(listener, executor);
+        executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId));
+    }
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the Shell, within the host process.
+     *
+     */
+    public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mLocalListeners.remove(listener);
+    }
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the host process.
+     */
+    public void setRemoteFocusTransitionListener(Transitions transitions,
+            IFocusTransitionListener listener) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mRemoteListener = listener;
+        notifyFocusedDisplayChangedToRemote();
+    }
+
+    /**
+     * Notifies the listener that display focus has changed.
+     */
+    public void notifyFocusedDisplayChanged() {
+        notifyFocusedDisplayChangedToRemote();
+        mLocalListeners.forEach((listener, executor) ->
+                executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)));
+    }
+
+    private void notifyFocusedDisplayChangedToRemote() {
+        if (mRemoteListener != null) {
+            try {
+                mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e);
+            }
+        }
+    }
+}
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 d03832d..d280dcd 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
@@ -87,6 +87,8 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
 import com.android.wm.shell.shared.IHomeTransitionListener;
 import com.android.wm.shell.shared.IShellTransitions;
 import com.android.wm.shell.shared.ShellTransitions;
@@ -103,6 +105,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.concurrent.Executor;
 
 /**
  * Plays transition animations. Within this player, each transition has a lifecycle.
@@ -224,6 +227,7 @@
     private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
 
     private HomeTransitionObserver mHomeTransitionObserver;
+    private FocusTransitionObserver mFocusTransitionObserver;
 
     /** List of {@link Runnable} instances to run when the last active transition has finished.  */
     private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
@@ -309,10 +313,12 @@
             @NonNull ShellExecutor mainExecutor,
             @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor,
-            @NonNull HomeTransitionObserver observer) {
+            @NonNull HomeTransitionObserver homeTransitionObserver,
+            @NonNull FocusTransitionObserver focusTransitionObserver) {
         this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
                 displayController, mainExecutor, mainHandler, animExecutor,
-                new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer);
+                new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
+                homeTransitionObserver, focusTransitionObserver);
     }
 
     public Transitions(@NonNull Context context,
@@ -326,7 +332,8 @@
             @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor,
             @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
-            @NonNull HomeTransitionObserver observer) {
+            @NonNull HomeTransitionObserver homeTransitionObserver,
+            @NonNull FocusTransitionObserver focusTransitionObserver) {
         mOrganizer = organizer;
         mContext = context;
         mMainExecutor = mainExecutor;
@@ -345,7 +352,8 @@
         mHandlers.add(mRemoteTransitionHandler);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
         shellInit.addInitCallback(this::onInit, this);
-        mHomeTransitionObserver = observer;
+        mHomeTransitionObserver = homeTransitionObserver;
+        mFocusTransitionObserver = focusTransitionObserver;
 
         if (android.tracing.Flags.perfettoTransitionTracing()) {
             mTransitionTracer = new PerfettoTransitionTracer();
@@ -384,6 +392,8 @@
 
         mShellCommandHandler.addCommandCallback("transitions", this, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
+
+        registerObserver(mFocusTransitionObserver);
     }
 
     public boolean isRegistered() {
@@ -1573,6 +1583,21 @@
             mMainExecutor.execute(
                     () -> mRemoteTransitionHandler.removeFiltered(remoteTransition));
         }
+
+        @Override
+        public void setFocusTransitionListener(FocusTransitionListener listener,
+                Executor executor) {
+            mMainExecutor.execute(() ->
+                    mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor));
+
+        }
+
+        @Override
+        public void unsetFocusTransitionListener(FocusTransitionListener listener) {
+            mMainExecutor.execute(() ->
+                    mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener));
+
+        }
     }
 
     /**
@@ -1634,6 +1659,15 @@
         }
 
         @Override
+        public void setFocusTransitionListener(IFocusTransitionListener listener) {
+            executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener",
+                    (transitions) -> {
+                        transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener(
+                                transitions, listener);
+                    });
+        }
+
+        @Override
         public SurfaceControl getHomeTaskOverlayContainer() {
             SurfaceControl[] result = new SurfaceControl[1];
             executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 226b0fb..1be26f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -107,4 +107,27 @@
         }
         windowManagerWrapper.updateViewLayout(view, lp)
     }
+
+    class Factory {
+        fun create(
+            windowManagerWrapper: WindowManagerWrapper,
+            taskId: Int,
+            x: Int,
+            y: Int,
+            width: Int,
+            height: Int,
+            flags: Int,
+            view: View,
+        ): AdditionalSystemViewContainer =
+            AdditionalSystemViewContainer(
+                windowManagerWrapper = windowManagerWrapper,
+                taskId = taskId,
+                x = x,
+                y = y,
+                width = width,
+                height = height,
+                flags = flags,
+                view = view
+            )
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
new file mode 100644
index 0000000..98413ee
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.education
+
+import android.annotation.DimenRes
+import android.annotation.LayoutRes
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.util.Size
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.WindowManager
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.R
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.windowdecor.WindowManagerWrapper
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+
+/**
+ * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that
+ * only one tooltip is displayed at a time.
+ */
+class DesktopWindowingEducationTooltipController(
+    private val context: Context,
+    private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
+) {
+  // TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme
+  private var tooltipView: View? = null
+  private var animator: PhysicsAnimator<View>? = null
+  private val springConfig by lazy {
+    PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+  }
+  private var popupWindow: AdditionalSystemViewContainer? = null
+
+  /**
+   * Shows education tooltip.
+   *
+   * @param tooltipViewConfig features of tooltip.
+   * @param taskId is used in the title of popup window created for the tooltip view.
+   */
+  fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) {
+    hideEducationTooltip()
+    tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
+    animator = createAnimator()
+    animateShowTooltipTransition()
+  }
+
+  /** Hide the current education view if visible */
+  private fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() }
+
+  /** Create education view by inflating layout provided. */
+  private fun createEducationTooltipView(
+      tooltipViewConfig: EducationViewConfig,
+      taskId: Int,
+  ): View {
+    val tooltipView =
+        LayoutInflater.from(context)
+            .inflate(
+                tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false)
+            .apply {
+              alpha = 0f
+              scaleX = 0f
+              scaleY = 0f
+
+              requireViewById<TextView>(R.id.tooltip_text).apply {
+                text = tooltipViewConfig.tooltipText
+              }
+
+              setOnTouchListener { _, motionEvent ->
+                if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) {
+                  hideEducationTooltip()
+                  tooltipViewConfig.onDismissAction()
+                  true
+                } else {
+                  false
+                }
+              }
+              setOnClickListener {
+                hideEducationTooltip()
+                tooltipViewConfig.onEducationClickAction()
+              }
+            }
+
+    val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection)
+    val tooltipViewGlobalCoordinates =
+        tooltipViewGlobalCoordinates(
+            tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates,
+            arrowDirection = tooltipViewConfig.arrowDirection,
+            tooltipDimen = tooltipDimens)
+    createTooltipPopupWindow(
+        taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView)
+
+    return tooltipView
+  }
+
+  /** Create animator for education transitions */
+  private fun createAnimator(): PhysicsAnimator<View>? =
+      tooltipView?.let {
+        PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
+      }
+
+  /** Animate show transition for the education view */
+  private fun animateShowTooltipTransition() {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 1f)
+        ?.spring(DynamicAnimation.SCALE_X, 1f)
+        ?.spring(DynamicAnimation.SCALE_Y, 1f)
+        ?.start()
+  }
+
+  /** Animate hide transition for the education view */
+  private fun animateHideTooltipTransition(endActions: () -> Unit) {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 0f)
+        ?.spring(DynamicAnimation.SCALE_X, 0f)
+        ?.spring(DynamicAnimation.SCALE_Y, 0f)
+        ?.start()
+    endActions()
+  }
+
+  /** Remove education tooltip and clean up all relative properties */
+  private fun cleanUp() {
+    tooltipView = null
+    animator = null
+    popupWindow?.releaseView()
+    popupWindow = null
+  }
+
+  private fun createTooltipPopupWindow(
+      taskId: Int,
+      tooltipViewGlobalCoordinates: Point,
+      tooltipDimen: Size,
+      tooltipView: View,
+  ) {
+    popupWindow =
+        additionalSystemViewContainerFactory.create(
+            windowManagerWrapper =
+                WindowManagerWrapper(context.getSystemService(WindowManager::class.java)),
+            taskId = taskId,
+            x = tooltipViewGlobalCoordinates.x,
+            y = tooltipViewGlobalCoordinates.y,
+            width = tooltipDimen.width,
+            height = tooltipDimen.height,
+            flags =
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+            view = tooltipView)
+  }
+
+  private fun tooltipViewGlobalCoordinates(
+      tooltipViewGlobalCoordinates: Point,
+      arrowDirection: TooltipArrowDirection,
+      tooltipDimen: Size,
+  ): Point {
+    var tooltipX = tooltipViewGlobalCoordinates.x
+    var tooltipY = tooltipViewGlobalCoordinates.y
+
+    // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow.
+    // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of
+    // the window to be created. Hence we will need to move the coordinates left/up in order
+    // to position the tooltip correctly.
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement
+      // half of tooltip width from [tooltipX] to horizontally position the tooltip.
+      tooltipX -= tooltipDimen.width / 2
+    } else {
+      // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement
+      // half of tooltip height from [tooltipY] to vertically position the tooltip.
+      tooltipY -= tooltipDimen.height / 2
+    }
+    return Point(tooltipX, tooltipY)
+  }
+
+  private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size {
+    val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container)
+    val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon)
+    tooltipBackground.measure(
+        /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+    arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+
+    var desiredWidth =
+        tooltipBackground.measuredWidth +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    var desiredHeight =
+        tooltipBackground.measuredHeight +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // desiredHeight currently does not account for the height of arrow, hence adding it.
+      desiredHeight += arrowView.height
+    } else {
+      // desiredWidth currently does not account for the width of arrow, hence adding it.
+      desiredWidth += arrowView.width
+    }
+
+    return Size(desiredWidth, desiredHeight)
+  }
+
+  private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
+    if (resourceId == Resources.ID_NULL) return 0
+    return context.resources.getDimensionPixelSize(resourceId)
+  }
+
+  /**
+   * The configuration for education view features:
+   *
+   * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip.
+   * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip
+   *   arrow.
+   * @property tooltipText Text to be added to the TextView of tooltip.
+   * @property arrowDirection Direction of arrow of the tooltip.
+   * @property onEducationClickAction Lambda to be executed when the tooltip is clicked.
+   * @property onDismissAction Lambda to be executed when the tooltip is dismissed.
+   */
+  data class EducationViewConfig(
+      @LayoutRes val tooltipViewLayout: Int,
+      val tooltipViewGlobalCoordinates: Point,
+      val tooltipText: String,
+      val arrowDirection: TooltipArrowDirection,
+      val onEducationClickAction: () -> Unit,
+      val onDismissAction: () -> Unit,
+  )
+
+  /** Direction of arrow of the tooltip */
+  enum class TooltipArrowDirection {
+    UP,
+    LEFT,
+  }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
index a2df22c..40b685c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell;
 
 import static android.view.Display.DEFAULT_DISPLAY;
+
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
@@ -45,6 +46,9 @@
         // Disable protolog tool when running the tests from studio
         ProtoLog.REQUIRE_PROTOLOGTOOL = false;
 
+        // Make sure ProtoLog is initialized before any logging occurs.
+        ProtoLog.init();
+
         MockitoAnnotations.initMocks(this);
         final Context context =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 177e47a..c52d9dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -19,7 +19,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -136,7 +136,7 @@
     @Test
     public void testSetDivideRatio() {
         mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */);
-        mSplitLayout.setDivideRatio(SNAP_TO_50_50);
+        mSplitLayout.setDivideRatio(SNAP_TO_2_50_50);
         assertThat(mSplitLayout.getDividerPosition()).isEqualTo(
                 mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ee54520..94e3616 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -40,6 +40,7 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Binder
+import android.os.Bundle
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
@@ -2086,16 +2087,13 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
-    assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token)
+    assertNull(result, "Should not handle request")
   }
 
   @Test
@@ -2137,26 +2135,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() {
+  fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -2183,23 +2163,7 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
   fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
@@ -2226,29 +2190,11 @@
 
     // Should create remove wallpaper transaction
     assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
   @EnableFlags(
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
   fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2261,23 +2207,6 @@
 
     // Should create remove wallpaper transaction
     assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
@@ -2937,6 +2866,108 @@
   }
 
   @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFullscreenOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenNewWindow(task)
+    verify(splitScreenController)
+      .startIntent(any(), anyInt(), any(), any(),
+        optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromSplitOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpSplitScreenTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenNewWindow(task)
+    verify(splitScreenController)
+      .startIntent(
+        any(), anyInt(), any(), any(),
+        optionsCaptor.capture(), anyOrNull()
+      )
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFreeformAddsNewWindow() {
+    setUpLandscapeDisplay()
+    val task = setUpFreeformTask()
+    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    runOpenNewWindow(task)
+    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+      .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+  }
+
+  private fun runOpenNewWindow(task: RunningTaskInfo) {
+    markTaskVisible(task)
+    task.baseActivity = mock(ComponentName::class.java)
+    task.isFocused = true
+    runningTasks.add(task)
+    controller.openNewWindow(task)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromFullscreenOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask()
+    val taskToRequest = setUpFreeformTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(splitScreenController)
+      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromSplitOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpSplitScreenTask()
+    val taskToRequest = setUpFreeformTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(splitScreenController)
+      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromFreeformAddsNewWindow() {
+    setUpLandscapeDisplay()
+    val task = setUpFreeformTask()
+    val taskToRequest = setUpFreeformTask()
+    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+      .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+  }
+
+  private fun runOpenInstance(
+    callingTask: RunningTaskInfo,
+    requestedTaskId: Int
+  ) {
+    markTaskVisible(callingTask)
+    callingTask.baseActivity = mock(ComponentName::class.java)
+    callingTask.isFocused = true
+    runningTasks.add(callingTask)
+    controller.openInstance(callingTask, requestedTaskId)
+  }
+
+  @Test
   fun toggleBounds_togglesToStableBounds() {
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
new file mode 100644
index 0000000..c989d16
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class DesktopTasksTransitionObserverTest {
+
+    @JvmField
+    @Rule
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this)
+            .mockStatic(DesktopModeStatus::class.java)
+            .build()!!
+
+    private val testExecutor = mock<ShellExecutor>()
+    private val mockShellInit = mock<ShellInit>()
+    private val transitions = mock<Transitions>()
+    private val context = mock<Context>()
+    private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+    private val taskRepository = mock<DesktopModeTaskRepository>()
+
+    private lateinit var transitionObserver: DesktopTasksTransitionObserver
+    private lateinit var shellInit: ShellInit
+
+    @Before
+    fun setup() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+        shellInit = spy(ShellInit(testExecutor))
+
+        transitionObserver =
+            DesktopTasksTransitionObserver(
+                context, taskRepository, transitions, shellTaskOrganizer, shellInit
+            )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun backNavigation_taskMinimized() {
+        val task = createTaskInfo(1)
+        whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info =
+            createBackNavigationTransition(task),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).minimizeTask(task.displayId, task.taskId)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun backNavigation_nullTaskInfo_taskNotMinimized() {
+        val task = createTaskInfo(1)
+        whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info =
+            createBackNavigationTransition(null),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
+    }
+
+    private fun createBackNavigationTransition(
+        task: RunningTaskInfo?
+    ): TransitionInfo {
+        return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+            addChange(
+                Change(mock(), mock()).apply {
+                    mode = TRANSIT_TO_BACK
+                    parent = null
+                    taskInfo = task
+                    flags = flags
+                }
+            )
+        }
+    }
+
+    private fun createTaskInfo(id: Int) =
+        RunningTaskInfo().apply {
+            taskId = id
+            displayId = DEFAULT_DISPLAY
+            configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java))
+            baseIntent = Intent().apply {
+                component = ComponentName("package", "component.name")
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
index 0c3f98a..0c100fc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
@@ -30,7 +30,7 @@
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
@@ -136,7 +136,7 @@
         assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
         assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
         assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
-        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_50_50)
+        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
     }
 
     @Test
@@ -185,7 +185,7 @@
     private fun splitTasksGroupInfo(): GroupedRecentTaskInfo {
         val task1 = createTaskInfo(id = 1)
         val task2 = createTaskInfo(id = 2)
-        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_50_50)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
         return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds)
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 386253c..753d4cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -22,7 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -211,10 +211,10 @@
 
         // Verify only one update if the split info is the same
         SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1);
         SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2);
         verify(mRecentTasksController, times(1)).notifyRecentTasksChanged();
     }
@@ -246,9 +246,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -277,9 +277,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -339,7 +339,7 @@
         setRawList(t1, t2, t3, t4, t5);
 
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
 
         when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
@@ -449,7 +449,7 @@
 
         // Add a pair
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds);
         reset(mRecentTasksController);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
index 248393c..be8e6dc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
@@ -1,6 +1,6 @@
 package com.android.wm.shell.recents;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -46,21 +46,21 @@
     @Test
     public void testVerticalStacked() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertTrue(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalStacked() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertFalse(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(0, dividerBounds.left);
         assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top);
@@ -71,7 +71,7 @@
     @Test
     public void testVerticalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left);
         assertEquals(0, dividerBounds.top);
@@ -82,7 +82,7 @@
     @Test
     public void testEqualVerticalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH;
         assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01);
     }
@@ -90,7 +90,7 @@
     @Test
     public void testEqualHorizontalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH;
         assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
index 19c18be..ac96063 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
@@ -42,19 +42,44 @@
             SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
         )
         assertEquals(
-            "the value of SNAP_TO_30_70 should be 0",
+            "the value of SNAP_TO_2_33_66 should be 0",
             0,
-            SplitScreenConstants.SNAP_TO_30_70,
+            SplitScreenConstants.SNAP_TO_2_33_66,
         )
         assertEquals(
-            "the value of SNAP_TO_50_50 should be 1",
+            "the value of SNAP_TO_2_50_50 should be 1",
             1,
-            SplitScreenConstants.SNAP_TO_50_50,
+            SplitScreenConstants.SNAP_TO_2_50_50,
         )
         assertEquals(
-            "the value of SNAP_TO_70_30 should be 2",
+            "the value of SNAP_TO_2_66_33 should be 2",
             2,
-            SplitScreenConstants.SNAP_TO_70_30,
+            SplitScreenConstants.SNAP_TO_2_66_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_90_10 should be 3",
+            3,
+            SplitScreenConstants.SNAP_TO_2_90_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_10_90 should be 4",
+            4,
+            SplitScreenConstants.SNAP_TO_2_10_90,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_33_33_33 should be 5",
+            5,
+            SplitScreenConstants.SNAP_TO_3_33_33_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_45_45_10 should be 6",
+            6,
+            SplitScreenConstants.SNAP_TO_3_45_45_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_10_45_45 should be 7",
+            7,
+            SplitScreenConstants.SNAP_TO_3_10_45_45,
         )
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index a6c16c4..67eda8b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -74,6 +74,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.HomeTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
 
@@ -429,7 +430,8 @@
         ShellInit shellInit = new ShellInit(mMainExecutor);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         return t;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
index 0c18229..e540322 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -25,11 +25,14 @@
 import static android.window.TransitionInfo.FLAG_SYNC;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.os.Binder;
@@ -37,6 +40,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.view.SurfaceControl;
+import android.view.animation.AlphaAnimation;
 import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
 
@@ -56,6 +60,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+
 /**
  * Tests for the default animation handler that is used if no other special-purpose handler picks
  * up an animation request.
@@ -187,6 +193,34 @@
     }
 
     @Test
+    public void testBuildSurfaceAnimation() {
+        final ArrayList<Animator> animators = new ArrayList<>();
+        final AlphaAnimation animation = new AlphaAnimation(0, 1);
+        final long durationMs = 500;
+        animation.setDuration(durationMs);
+        final long[] lastCurrentPlayTime = new long[1];
+        final int[] finishCount = new int[1];
+        final Runnable finishCallback = () -> finishCount[0]++;
+        DefaultSurfaceAnimator.buildSurfaceAnimation(animators, animation, finishCallback,
+                mTransactionPool, mMainExecutor,
+                new DefaultSurfaceAnimator.AnimationAdapter(mock(SurfaceControl.class)) {
+                    @Override
+                    void applyTransformation(ValueAnimator animator, long currentPlayTime) {
+                        lastCurrentPlayTime[0] = currentPlayTime;
+                    }
+                });
+        final ValueAnimator animator = (ValueAnimator) animators.get(0);
+        mAnimExecutor.execute(() -> {
+            animator.start();
+            animator.end();
+        });
+        flushHandlers();
+        assertEquals(durationMs, lastCurrentPlayTime[0]);
+        assertEquals(1f, animator.getAnimatedFraction(), 0f /* delta */);
+        assertEquals(1, finishCount[0]);
+    }
+
+    @Test
     public void startAnimation_freeformOpenChange_doesntReparentTask() {
         final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN)
                 .setTask(createTaskInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
new file mode 100644
index 0000000..d37b4cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.TransitionMode;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+import com.android.wm.shell.shared.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for the focus transition observer.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS)
+public class FocusTransitionObserverTest extends ShellTestCase {
+
+    static final int SECONDARY_DISPLAY_ID = 1;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    private IFocusTransitionListener mListener;
+    private Transitions mTransition;
+    private FocusTransitionObserver mFocusTransitionObserver;
+
+    @Before
+    public void setUp() {
+        mListener = mock(IFocusTransitionListener.class);
+        when(mListener.asBinder()).thenReturn(mock(IBinder.class));
+
+        mFocusTransitionObserver = new FocusTransitionObserver();
+        mTransition =
+                new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        mock(ShellInit.class), mock(ShellController.class),
+                        mock(ShellTaskOrganizer.class), mock(TransactionPool.class),
+                        mock(DisplayController.class), new TestShellExecutor(),
+                        new Handler(Looper.getMainLooper()), new TestShellExecutor(),
+                        mock(HomeTransitionObserver.class),
+                mFocusTransitionObserver);
+        mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener);
+    }
+
+    @Test
+    public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+        final IBinder binder = mock(IBinder.class);
+        final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
+
+        // Open a task on the default display, which doesn't change display focus because the
+        // default display already has it.
+        TransitionInfo info = mock(TransitionInfo.class);
+        final List<TransitionInfo.Change> changes = new ArrayList<>();
+        setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+        clearInvocations(mListener);
+
+        // Open a new task on the secondary display and verify display focus changes to the display.
+        changes.clear();
+        setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+        clearInvocations(mListener);
+
+        // Open the first task to front and verify display focus goes back to the default display.
+        changes.clear();
+        setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+        clearInvocations(mListener);
+
+        // Open another task on the default display and verify no display focus switch as it's
+        // already on the default display.
+        changes.clear();
+        setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+    }
+
+    private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+            @TransitionMode int mode, int displayId, boolean focused) {
+        TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+        RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
+        taskInfo.taskId = taskId;
+        taskInfo.isFocused = focused;
+        when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused);
+        taskInfo.displayId = displayId;
+        when(change.getTaskInfo()).thenReturn(taskInfo);
+        when(change.getMode()).thenReturn(mode);
+        changes.add(change);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 8f49de0..8dfdfb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -100,7 +100,8 @@
         mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor);
         mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class),
                 mOrganizer, mTransactionPool, mDisplayController, mMainExecutor,
-                mMainHandler, mAnimExecutor, mHomeTransitionObserver);
+                mMainHandler, mAnimExecutor, mHomeTransitionObserver,
+                mock(FocusTransitionObserver.class));
         mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index aea14b9..6cde056 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -158,7 +158,8 @@
         ShellInit shellInit = mock(ShellInit.class);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         // One from Transitions, one from RootTaskDisplayAreaOrganizer
         verify(shellInit).addInitCallback(any(), eq(t));
         verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class));
@@ -170,7 +171,8 @@
         ShellController shellController = mock(ShellController.class);
         final Transitions t = new Transitions(mContext, shellInit, shellController,
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         verify(shellController, times(1)).addExternalInterface(
                 eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any());
@@ -1238,7 +1240,8 @@
         final Transitions transitions =
                 new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
                         mTransactionPool, createTestDisplayController(), mMainExecutor,
-                        mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                        mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                        mock(FocusTransitionObserver.class));
         final RecentsTransitionHandler recentsHandler =
                 new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
                         mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
@@ -1780,7 +1783,8 @@
         ShellInit shellInit = new ShellInit(mMainExecutor);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         return t;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
new file mode 100644
index 0000000..5594981
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.education
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.graphics.Point
+import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() {
+  @Mock private lateinit var mockWindowManager: WindowManager
+  @Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory
+  private lateinit var testableResources: TestableResources
+  private lateinit var testableContext: TestableContext
+  private lateinit var tooltipController: DesktopWindowingEducationTooltipController
+  private val tooltipViewArgumentCaptor = argumentCaptor<View>()
+
+  @Before
+  fun setUp() {
+    MockitoAnnotations.initMocks(this)
+    testableContext = TestableContext(mContext)
+    testableResources =
+        testableContext.orCreateTestableResources.apply {
+          addOverride(R.dimen.desktop_windowing_education_tooltip_padding, 10)
+        }
+    testableContext.addMockSystemService(
+        Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+    testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager)
+    tooltipController =
+        DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory)
+  }
+
+  @Test
+  fun showEducationTooltip_createsTooltipWithCorrectText() {
+    val tooltipText = "This is a tooltip"
+    val tooltipViewConfig = createTooltipConfig(tooltipText = tooltipText)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val tooltipTextView =
+        tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text)
+    assertThat(tooltipTextView.text).isEqualTo(tooltipText)
+  }
+
+  @Test
+  fun showEducationTooltip_usesCorrectTaskIdForWindow() {
+    val tooltipViewConfig = createTooltipConfig()
+    val taskIdArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = taskIdArgumentCaptor.capture(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = anyOrNull())
+    assertThat(taskIdArgumentCaptor.lastValue).isEqualTo(123)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsUpwards_horizontallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.UP,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipXArgumentCaptor = argumentCaptor<Int>()
+    val tooltipWidthArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = tooltipXArgumentCaptor.capture(),
+            y = anyInt(),
+            width = tooltipWidthArgumentCaptor.capture(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipX = initialTooltipX - tooltipWidthArgumentCaptor.lastValue / 2
+    assertThat(tooltipXArgumentCaptor.lastValue).isEqualTo(expectedTooltipX)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsLeft_verticallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.LEFT,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipYArgumentCaptor = argumentCaptor<Int>()
+    val tooltipHeightArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = tooltipYArgumentCaptor.capture(),
+            width = anyInt(),
+            height = tooltipHeightArgumentCaptor.capture(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipY = initialTooltipY - tooltipHeightArgumentCaptor.lastValue / 2
+    assertThat(tooltipYArgumentCaptor.lastValue).isEqualTo(expectedTooltipY)
+  }
+
+  @Test
+  fun showEducationTooltip_touchEventActionOutside_dismissActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onDismissAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val motionEvent =
+        MotionEvent.obtain(
+            /* downTime= */ 0L,
+            /* eventTime= */ 0L,
+            MotionEvent.ACTION_OUTSIDE,
+            /* x= */ 0f,
+            /* y= */ 0f,
+            /* metaState= */ 0)
+    tooltipViewArgumentCaptor.lastValue.dispatchTouchEvent(motionEvent)
+
+    verify(mockLambda).invoke()
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipClicked_onClickActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onEducationClickAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    tooltipViewArgumentCaptor.lastValue.performClick()
+
+    verify(mockLambda).invoke()
+  }
+
+  private fun createTooltipConfig(
+      @LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip,
+      tooltipViewGlobalCoordinates: Point = Point(0, 0),
+      tooltipText: String = "This is a tooltip",
+      arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP,
+      onEducationClickAction: () -> Unit = {},
+      onDismissAction: () -> Unit = {}
+  ) =
+      DesktopWindowingEducationTooltipController.EducationViewConfig(
+          tooltipViewLayout = tooltipViewLayout,
+          tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates,
+          tooltipText = tooltipText,
+          arrowDirection = arrowDirection,
+          onEducationClickAction = onEducationClickAction,
+          onDismissAction = onDismissAction,
+      )
+}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index 504e329..3ed33db 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -3,13 +3,15 @@
 
   public final class AppFunctionManager {
     ctor public AppFunctionManager(android.content.Context);
-    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
   }
 
   public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
     field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index b1dd467..815fe05 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -19,12 +19,12 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.CancellationSignal;
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
-
 /**
  * Provides app functions related functionalities.
  *
@@ -45,7 +45,7 @@
      *
      * @param context A {@link Context}.
      * @throws java.lang.IllegalStateException if the underlying {@link
-     *   android.app.appfunctions.AppFunctionManager} is not found.
+     *     android.app.appfunctions.AppFunctionManager} is not found.
      */
     public AppFunctionManager(Context context) {
         mContext = Objects.requireNonNull(context);
@@ -66,6 +66,7 @@
     public void executeAppFunction(
             @NonNull ExecuteAppFunctionRequest sidecarRequest,
             @NonNull @CallbackExecutor Executor executor,
+            @NonNull CancellationSignal cancellationSignal,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
         Objects.requireNonNull(sidecarRequest);
         Objects.requireNonNull(executor);
@@ -74,9 +75,40 @@
         android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
                 SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
         mManager.executeAppFunction(
-                platformRequest, executor, (platformResponse) -> {
-                    callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
-                            platformResponse));
+                platformRequest,
+                executor,
+                cancellationSignal,
+                (platformResponse) -> {
+                    callback.accept(
+                            SidecarConverter.getSidecarExecuteAppFunctionResponse(
+                                    platformResponse));
                 });
     }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Proxies request and response to the underlying {@link
+     * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+     * response in the appropriate type required by the function.
+     *
+     * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+     *     CancellationSignal, Consumer)} instead. This method will be removed once usage references
+     *     are updated.
+     */
+    @Deprecated
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest sidecarRequest,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        Objects.requireNonNull(sidecarRequest);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        executeAppFunction(
+                sidecarRequest,
+                executor,
+                new CancellationSignal(),
+                callback);
+    }
 }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 65959df..6023c97 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -25,6 +25,7 @@
 import android.content.Intent;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.CancellationSignal;
 
 import java.util.function.Consumer;
 
@@ -69,10 +70,11 @@
     private final Binder mBinder =
             android.app.appfunctions.AppFunctionService.createBinder(
                     /* context= */ this,
-                    /* onExecuteFunction= */ (platformRequest, callback) -> {
+                    /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> {
                         AppFunctionService.this.onExecuteFunction(
                                 SidecarConverter.getSidecarExecuteAppFunctionRequest(
                                         platformRequest),
+                                cancellationSignal,
                                 (sidecarResponse) -> {
                                     callback.accept(
                                             SidecarConverter.getPlatformExecuteAppFunctionResponse(
@@ -105,9 +107,42 @@
      * result using the callback, no matter if the execution was successful or not.
      *
      * @param request The function execution request.
+     * @param cancellationSignal A {@link CancellationSignal} to cancel the request.
      * @param callback A callback to report back the result.
      */
     @MainThread
+    public void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        onExecuteFunction(request, callback);
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     *
+     * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+     *     Consumer)} instead. This method will be removed once usage references are updated.
+     */
+    @MainThread
+    @Deprecated
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback);
diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp
new file mode 100644
index 0000000..6f5eff3
--- /dev/null
+++ b/libs/appfunctions/tests/Android.bp
@@ -0,0 +1,41 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AppFunctionsSidecarTestCases",
+    team: "trendy_team_system_intelligence",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.core_core-ktx",
+        "com.google.android.appfunctions.sidecar.impl",
+        "junit",
+        "kotlin-test",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "testables",
+        "testng",
+        "truth",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+}
diff --git a/libs/appfunctions/tests/AndroidManifest.xml b/libs/appfunctions/tests/AndroidManifest.xml
new file mode 100644
index 0000000..9a7d460
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.appfunctions.sidecar.tests">
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.mock" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.google.android.appfunctions.sidecar.tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/appfunctions/tests/AndroidTest.xml b/libs/appfunctions/tests/AndroidTest.xml
new file mode 100644
index 0000000..8251212
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for AppFunctions Sidecar Tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="AppFunctionsSidecarTestCases.apk" />
+    </target_preparer>
+    <option name="test-tag" value="AppFunctionsSidecarTestCases" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.google.android.appfunctions.sidecar.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
new file mode 100644
index 0000000..1f9fddd
--- /dev/null
+++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.google.android.appfunctions.sidecar.tests
+
+import android.app.appfunctions.ExecuteAppFunctionRequest
+import android.app.appfunctions.ExecuteAppFunctionResponse
+import android.app.appsearch.GenericDocument
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.appfunctions.sidecar.SidecarConverter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SidecarConverterTest {
+    @Test
+    fun getSidecarExecuteAppFunctionRequest_sameContents() {
+        val extras = Bundle()
+        extras.putString("extra", "value")
+        val parameters: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("testLong", 23)
+                .build()
+        val platformRequest: ExecuteAppFunctionRequest =
+            ExecuteAppFunctionRequest.Builder("targetPkg", "targetFunctionId")
+                .setExtras(extras)
+                .setParameters(parameters)
+                .build()
+
+        val sidecarRequest = SidecarConverter.getSidecarExecuteAppFunctionRequest(platformRequest)
+
+        assertThat(sidecarRequest.targetPackageName).isEqualTo("targetPkg")
+        assertThat(sidecarRequest.functionIdentifier).isEqualTo("targetFunctionId")
+        assertThat(sidecarRequest.parameters).isEqualTo(parameters)
+        assertThat(sidecarRequest.extras.size()).isEqualTo(1)
+        assertThat(sidecarRequest.extras.getString("extra")).isEqualTo("value")
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionRequest_sameContents() {
+        val extras = Bundle()
+        extras.putString("extra", "value")
+        val parameters: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("testLong", 23)
+                .build()
+        val sidecarRequest =
+            com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder(
+                "targetPkg",
+                "targetFunctionId"
+            )
+                .setExtras(extras)
+                .setParameters(parameters)
+                .build()
+
+        val platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest)
+
+        assertThat(platformRequest.targetPackageName).isEqualTo("targetPkg")
+        assertThat(platformRequest.functionIdentifier).isEqualTo("targetFunctionId")
+        assertThat(platformRequest.parameters).isEqualTo(parameters)
+        assertThat(platformRequest.extras.size()).isEqualTo(1)
+        assertThat(platformRequest.extras.getString("extra")).isEqualTo("value")
+    }
+
+    @Test
+    fun getSidecarExecuteAppFunctionResponse_successResponse_sameContents() {
+        val resultGd: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+                .build()
+        val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null)
+
+        val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+            platformResponse
+        )
+
+        assertThat(sidecarResponse.isSuccess).isTrue()
+        assertThat(
+            sidecarResponse.resultDocument.getProperty(
+                ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+            )
+        )
+            .isEqualTo(booleanArrayOf(true))
+        assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+        assertThat(sidecarResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() {
+        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+        val platformResponse =
+            ExecuteAppFunctionResponse.newFailure(
+                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                null,
+                null
+            )
+
+        val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+            platformResponse
+        )
+
+        assertThat(sidecarResponse.isSuccess).isFalse()
+        assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+        assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id)
+        assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+        assertThat(sidecarResponse.resultCode)
+            .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+        assertThat(sidecarResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionResponse_successResponse_sameContents() {
+        val resultGd: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+                .build()
+        val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse
+            .newSuccess(resultGd, null)
+
+        val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+            sidecarResponse
+        )
+
+        assertThat(platformResponse.isSuccess).isTrue()
+        assertThat(
+            platformResponse.resultDocument.getProperty(
+                ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+            )
+        )
+            .isEqualTo(booleanArrayOf(true))
+        assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+        assertThat(platformResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() {
+        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+        val sidecarResponse =
+            com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure(
+                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                null,
+                null
+            )
+
+        val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+            sidecarResponse
+        )
+
+        assertThat(platformResponse.isSuccess).isFalse()
+        assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+        assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id)
+        assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+        assertThat(platformResponse.resultCode)
+            .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+        assertThat(platformResponse.errorMessage).isNull()
+    }
+}
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index b6476c9..ae46a99 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -50,6 +50,10 @@
 constexpr bool resample_gainmap_regions() {
     return false;
 }
+
+constexpr bool query_global_priority() {
+    return false;
+}
 }  // namespace hwui_flags
 #endif
 
@@ -110,6 +114,7 @@
 bool Properties::hdr10bitPlus = false;
 bool Properties::skipTelemetry = false;
 bool Properties::resampleGainmapRegions = false;
+bool Properties::queryGlobalPriority = false;
 
 int Properties::timeoutMultiplier = 1;
 
@@ -187,6 +192,7 @@
     hdr10bitPlus = hwui_flags::hdr_10bit_plus();
     resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions",
                                                    hwui_flags::resample_gainmap_regions());
+    queryGlobalPriority = hwui_flags::query_global_priority();
 
     timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
     skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY,
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index db47152..6f84796 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -346,6 +346,7 @@
     static bool hdr10bitPlus;
     static bool skipTelemetry;
     static bool resampleGainmapRegions;
+    static bool queryGlobalPriority;
 
     static int timeoutMultiplier;
 
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ab052b9..93df478 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -129,3 +129,13 @@
   description: "APIs that expose gainmap metadata corresponding to those defined in ISO 21496-1"
   bug: "349357636"
 }
+
+flag {
+  name: "query_global_priority"
+  namespace: "core_graphics"
+  description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation."
+  bug: "343986434"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp
index 3884342..e9de655 100644
--- a/libs/hwui/jni/PathIterator.cpp
+++ b/libs/hwui/jni/PathIterator.cpp
@@ -20,6 +20,7 @@
 #include "GraphicsJNI.h"
 #include "SkPath.h"
 #include "SkPoint.h"
+#include "graphics_jni_helpers.h"
 
 namespace android {
 
@@ -36,6 +37,18 @@
         return reinterpret_cast<jlong>(new SkPath::RawIter(*path));
     }
 
+    // A variant of 'next' (below) that is compatible with the host JVM.
+    static jint nextHost(JNIEnv* env, jclass clazz, jlong iteratorHandle, jfloatArray pointsArray) {
+        jfloat* points = env->GetFloatArrayElements(pointsArray, 0);
+#ifdef __ANDROID__
+        jint result = next(iteratorHandle, reinterpret_cast<jlong>(points));
+#else
+        jint result = next(env, clazz, iteratorHandle, reinterpret_cast<jlong>(points));
+#endif
+        env->ReleaseFloatArrayElements(pointsArray, points, 0);
+        return result;
+    }
+
     // ---------------- @CriticalNative -------------------------
 
     static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) {
@@ -72,6 +85,7 @@
 
         {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek},
         {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next},
+        {"nNextHost", "(J[F)I", (void*)SkPathIteratorGlue::nextHost},
 };
 
 int register_android_graphics_PathIterator(JNIEnv* env) {
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index e302393..6571d92 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -44,7 +44,7 @@
 namespace renderthread {
 
 // Not all of these are strictly required, but are all enabled if present.
-static std::array<std::string_view, 21> sEnableExtensions{
+static std::array<std::string_view, 23> sEnableExtensions{
         VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
         VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
         VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -65,6 +65,8 @@
         VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
         VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
         VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
+        VK_EXT_GLOBAL_PRIORITY_QUERY_EXTENSION_NAME,
+        VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME,
         VK_EXT_DEVICE_FAULT_EXTENSION_NAME,
 };
 
@@ -206,7 +208,7 @@
     GET_INST_PROC(GetPhysicalDeviceFeatures2);
     GET_INST_PROC(GetPhysicalDeviceImageFormatProperties2);
     GET_INST_PROC(GetPhysicalDeviceProperties);
-    GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties);
+    GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties2);
 
     uint32_t gpuCount;
     LOG_ALWAYS_FATAL_IF(mEnumeratePhysicalDevices(mInstance, &gpuCount, nullptr));
@@ -225,21 +227,30 @@
 
     // query to get the initial queue props size
     uint32_t queueCount = 0;
-    mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr);
+    mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, nullptr);
     LOG_ALWAYS_FATAL_IF(!queueCount);
 
     // now get the actual queue props
-    std::unique_ptr<VkQueueFamilyProperties[]> queueProps(new VkQueueFamilyProperties[queueCount]);
-    mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, queueProps.get());
+    std::unique_ptr<VkQueueFamilyProperties2[]>
+            queueProps(new VkQueueFamilyProperties2[queueCount]);
+    // query the global priority, this ignored if VK_EXT_global_priority isn't supported
+    std::vector<VkQueueFamilyGlobalPriorityPropertiesEXT> queuePriorityProps(queueCount);
+    for (uint32_t i = 0; i < queueCount; i++) {
+        queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
+        queuePriorityProps[i].pNext = nullptr;
+        queueProps[i].pNext = &queuePriorityProps[i];
+    }
+    mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
 
     constexpr auto kRequestedQueueCount = 2;
 
     // iterate to find the graphics queue
     mGraphicsQueueIndex = queueCount;
     for (uint32_t i = 0; i < queueCount; i++) {
-        if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
+        if (queueProps[i].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
             mGraphicsQueueIndex = i;
-            LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < kRequestedQueueCount);
+            LOG_ALWAYS_FATAL_IF(
+                    queueProps[i].queueFamilyProperties.queueCount < kRequestedQueueCount);
             break;
         }
     }
@@ -327,6 +338,15 @@
         tailPNext = &formatFeatures->pNext;
     }
 
+    VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT* globalPriorityQueryFeatures =
+            new VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT;
+    globalPriorityQueryFeatures->sType =
+            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES_EXT;
+    globalPriorityQueryFeatures->pNext = nullptr;
+    globalPriorityQueryFeatures->globalPriorityQuery = false;
+    *tailPNext = globalPriorityQueryFeatures;
+    tailPNext = &globalPriorityQueryFeatures->pNext;
+
     // query to get the physical device features
     mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features);
     // this looks like it would slow things down,
@@ -341,24 +361,59 @@
 
     if (Properties::contextPriority != 0 &&
         grExtensions.hasExtension(VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, 2)) {
-        memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
-        queuePriorityCreateInfo.sType =
-                VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
-        queuePriorityCreateInfo.pNext = nullptr;
+        VkQueueGlobalPriorityEXT globalPriority;
         switch (Properties::contextPriority) {
             case EGL_CONTEXT_PRIORITY_LOW_IMG:
-                queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
+                globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
                 break;
             case EGL_CONTEXT_PRIORITY_MEDIUM_IMG:
-                queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
+                globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
                 break;
             case EGL_CONTEXT_PRIORITY_HIGH_IMG:
-                queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
+                globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
                 break;
             default:
                 LOG_ALWAYS_FATAL("Unsupported context priority");
         }
-        queueNextPtr = &queuePriorityCreateInfo;
+
+        // check if the requested priority is reported by the query
+        bool attachGlobalPriority = false;
+        if (uirenderer::Properties::queryGlobalPriority &&
+            globalPriorityQueryFeatures->globalPriorityQuery) {
+            for (uint32_t i = 0; i < queuePriorityProps[mGraphicsQueueIndex].priorityCount; i++) {
+                if (queuePriorityProps[mGraphicsQueueIndex].priorities[i] == globalPriority) {
+                    attachGlobalPriority = true;
+                    break;
+                }
+            }
+        } else {
+            // Querying is not supported so attempt queue creation with requested priority anyways
+            // If the priority turns out not to be supported, the driver *may* fail with
+            // VK_ERROR_NOT_PERMITTED_KHR
+            attachGlobalPriority = true;
+        }
+
+        if (attachGlobalPriority) {
+            memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
+            queuePriorityCreateInfo.sType =
+                    VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
+            queuePriorityCreateInfo.pNext = nullptr;
+            queuePriorityCreateInfo.globalPriority = globalPriority;
+            queueNextPtr = &queuePriorityCreateInfo;
+        } else {
+            // If globalPriorityQuery is enabled, attempting queue creation with an unsupported
+            // priority will return VK_ERROR_INITIALIZATION_FAILED.
+            //
+            // SysUI and Launcher will request HIGH when SF has RT but it is a known issue that
+            // upstream drm drivers currently lack a way to grant them the granular privileges
+            // they need for HIGH (but not RT) so they will fail queue creation.
+            // For now, drop the unsupported global priority request so that queue creation
+            // succeeds.
+            //
+            // Once that is fixed, this should probably be a fatal error indicating an improper
+            // request or an app needs to get the correct privileges.
+            ALOGW("Requested context priority is not supported by the queue");
+        }
     }
 
     const VkDeviceQueueCreateInfo queueInfo = {
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index f042571..a593ec6 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -152,7 +152,7 @@
     VkPtr<PFN_vkDestroyInstance> mDestroyInstance;
     VkPtr<PFN_vkEnumeratePhysicalDevices> mEnumeratePhysicalDevices;
     VkPtr<PFN_vkGetPhysicalDeviceProperties> mGetPhysicalDeviceProperties;
-    VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties> mGetPhysicalDeviceQueueFamilyProperties;
+    VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties2> mGetPhysicalDeviceQueueFamilyProperties2;
     VkPtr<PFN_vkGetPhysicalDeviceFeatures2> mGetPhysicalDeviceFeatures2;
     VkPtr<PFN_vkGetPhysicalDeviceImageFormatProperties2> mGetPhysicalDeviceImageFormatProperties2;
     VkPtr<PFN_vkCreateDevice> mCreateDevice;
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 31f8996..ef4c3ef 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -16,6 +16,8 @@
 
 package android.media.projection;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.compat.CompatChanges;
@@ -29,6 +31,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
@@ -70,6 +73,7 @@
     private final DisplayManager mDisplayManager;
     @NonNull
     private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>();
+    private final int mDisplayId;
 
     /** @hide */
     public MediaProjection(Context context, IMediaProjection impl) {
@@ -88,6 +92,11 @@
             throw new RuntimeException("Failed to start media projection", e);
         }
         mDisplayManager = displayManager;
+
+        final UserManager userManager = context.getSystemService(UserManager.class);
+        mDisplayId = userManager.isVisibleBackgroundUsersSupported()
+                ? userManager.getMainDisplayIdAssignedToUser()
+                : DEFAULT_DISPLAY;
     }
 
     /**
@@ -156,6 +165,7 @@
         if (surface != null) {
             builder.setSurface(surface);
         }
+        builder.setDisplayIdToMirror(mDisplayId);
         return createVirtualDisplay(builder, callback, handler);
     }
 
@@ -234,6 +244,7 @@
         if (surface != null) {
             builder.setSurface(surface);
         }
+        builder.setDisplayIdToMirror(mDisplayId);
         return createVirtualDisplay(builder, callback, handler);
     }
 
diff --git a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
index c376f25..8b28347 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
@@ -17,6 +17,7 @@
 package android.media.soundtrigger;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -328,6 +329,7 @@
          * Get the recognition config used to start this recognition.
          * @return - The config passed to the HAL for startRecognition.
          */
+        @FlaggedApi(Flags.FLAG_MANAGER_API)
         public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() {
             return mRecognitionConfig;
         }
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 78352871..3d0c406 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -19,6 +19,7 @@
 import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -42,7 +43,6 @@
 import android.media.permission.Identity;
 import android.media.permission.SafeCloseable;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -463,13 +463,18 @@
     public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
 
     /**
-     * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
-     * an error/the system service is restarted.
-     * @hide
+     * Loads a given sound model into the sound trigger.
+     *
+     * <p><b>Note:</b> the model will be unloaded if there is an error/the system service is
+     * restarted.
+     *
+     * @return {@link SoundTrigger#STATUS_OK} if the model was loaded successfully, error code
+     *         otherwise
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
-    @TestApi
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
     public int loadSoundModel(@NonNull SoundModel soundModel) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -508,11 +513,11 @@
      *
      * @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
      *         otherwise
-     *
-     * @hide
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
     public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
         @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
         Objects.requireNonNull(soundModelId);
@@ -531,11 +536,15 @@
 
     /**
      * Stops the given model's recognition.
-     * @hide
+     *
+     * @return {@link SoundTrigger#STATUS_OK} if the recognition could be stopped, error code
+     *         otherwise
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public int stopRecognition(UUID soundModelId) {
+    @UnsupportedAppUsage
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
+    public int stopRecognition(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
         }
@@ -548,12 +557,18 @@
     }
 
     /**
-     * Removes the given model from memory. Will also stop any pending recognitions.
-     * @hide
+     * Removes the given model from memory.
+     *
+     * <p>Will also stop any pending recognitions.
+     *
+     * @return {@link SoundTrigger#STATUS_OK} if the model was unloaded successfully, error code
+     *         otherwise
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public int unloadSoundModel(UUID soundModelId) {
+    @UnsupportedAppUsage
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
+    public int unloadSoundModel(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
         }
@@ -566,12 +581,13 @@
     }
 
     /**
-     * Returns true if the given model has had detection started on it.
-     * @hide
+     * Returns whether the given model has had detection started on it.
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
-    public boolean isRecognitionActive(UUID soundModelId) {
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
+    public boolean isRecognitionActive(@NonNull UUID soundModelId) {
         if (soundModelId == null || mSoundTriggerSession == null) {
             return false;
         }
@@ -599,14 +615,16 @@
     }
 
     /**
-     * Asynchronously get state of the indicated model.  The model state is returned as
-     * a recognition event in the callback that was registered in the startRecognition
-     * method.
-     * @hide
+     * Asynchronously gets state of the indicated model.
+     *
+     * <p>The model state is returned as a recognition event in the callback that was registered
+     * in the {@link #startRecognition(UUID, Bundle, ComponentName, RecognitionConfig)} method.
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
-    public int getModelState(UUID soundModelId) {
+    @FlaggedApi(Flags.FLAG_MANAGER_API)
+    public int getModelState(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
         }
@@ -621,7 +639,7 @@
     }
 
     /**
-     * Get the hardware sound trigger module properties currently loaded.
+     * Gets the hardware sound trigger module properties currently loaded.
      *
      * @return The properties currently loaded. Returns null if no supported hardware loaded.
      */
@@ -639,9 +657,11 @@
     }
 
     /**
-     * Set a model specific {@link ModelParams} with the given value. This
-     * parameter will keep its value for the duration the model is loaded regardless of starting and
-     * stopping recognition. Once the model is unloaded, the value will be lost.
+     * Sets a model specific {@link ModelParams} with the given value.
+     *
+     * <p>This parameter will keep its value for the duration the model is loaded regardless of
+     * starting and stopping recognition. Once the model is unloaded, the value will be lost.
+     *
      * {@link SoundTriggerManager#queryParameter} should be checked first before calling this
      * method.
      *
@@ -671,12 +691,15 @@
     }
 
     /**
-     * Get a model specific {@link ModelParams}. This parameter will keep its value
-     * for the duration the model is loaded regardless of starting and stopping recognition.
-     * Once the model is unloaded, the value will be lost. If the value is not set, a default
-     * value is returned. See {@link ModelParams} for parameter default values.
-     * {@link SoundTriggerManager#queryParameter} should be checked first before
-     * calling this method. Otherwise, an exception can be thrown.
+     * Gets a model specific {@link ModelParams}.
+     *
+     * <p>This parameter will keep its value for the duration the model is loaded regardless
+     * of starting and stopping recognition. Once the model is unloaded, the value will be lost.
+     * If the value is not set, a default value is returned. See {@link ModelParams} for
+     * parameter default values.
+     *
+     * {@link SoundTriggerManager#queryParameter} should be checked first before calling this
+     * method. Otherwise, an exception can be thrown.
      *
      * @param soundModelId UUID of model to get parameter
      * @param modelParam   {@link ModelParams}
@@ -697,9 +720,10 @@
     }
 
     /**
-     * Determine if parameter control is supported for the given model handle.
-     * This method should be checked prior to calling {@link SoundTriggerManager#setParameter} or
-     * {@link SoundTriggerManager#getParameter}.
+     * Determines if parameter control is supported for the given model handle.
+     *
+     * <p>This method should be checked prior to calling {@link SoundTriggerManager#setParameter}
+     * or {@link SoundTriggerManager#getParameter}.
      *
      * @param soundModelId handle of model to get parameter
      * @param modelParam {@link ModelParams}
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index c814c95..10423b9 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -56,3 +56,11 @@
     description: "Enhance HDMI-CEC power state and activeness transitions"
     bug: "332780751"
 }
+
+flag {
+    name: "media_quality_fw"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Media Quality V1.0 APIs for Android W"
+    bug: "348412562"
+}
diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp
index 6776f61..33650d9 100644
--- a/media/jni/android_media_ImageWriter.cpp
+++ b/media/jni/android_media_ImageWriter.cpp
@@ -735,10 +735,15 @@
 }
 
 static status_t attachAndQeueuGraphicBuffer(JNIEnv* env, JNIImageWriterContext *ctx,
-        sp<Surface> surface, sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace,
+        sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace,
         jint left, jint top, jint right, jint bottom, jint transform, jint scalingMode) {
     status_t res = OK;
     // Step 1. Attach Image
+    sp<Surface> surface = ctx->getProducer();
+    if (surface == nullptr) {
+        jniThrowException(env, "java/lang/IllegalStateException",
+                "Producer surface is null, ImageWriter seems already closed");
+    }
     res = surface->attachBuffer(gb.get());
     if (res != OK) {
         ALOGE("Attach image failed: %s (%d)", strerror(-res), res);
@@ -835,7 +840,6 @@
         return -1;
     }
 
-    sp<Surface> surface = ctx->getProducer();
     if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) {
         jniThrowException(env, "java/lang/IllegalStateException",
                 "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa");
@@ -851,7 +855,7 @@
         return -1;
     }
 
-    return attachAndQeueuGraphicBuffer(env, ctx, surface, buffer->mGraphicBuffer, timestampNs,
+    return attachAndQeueuGraphicBuffer(env, ctx, buffer->mGraphicBuffer, timestampNs,
             dataSpace, left, top, right, bottom, transform, scalingMode);
 }
 
@@ -866,7 +870,6 @@
         return -1;
     }
 
-    sp<Surface> surface = ctx->getProducer();
     if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) {
         jniThrowException(env, "java/lang/IllegalStateException",
                 "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa");
@@ -880,7 +883,8 @@
                 "Trying to attach an invalid graphic buffer");
         return -1;
     }
-    return attachAndQeueuGraphicBuffer(env, ctx, surface, graphicBuffer, timestampNs,
+
+    return attachAndQeueuGraphicBuffer(env, ctx, graphicBuffer, timestampNs,
             dataSpace, left, top, right, bottom, transform, scalingMode);
 }
 
diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp
index 71ecb4c..37a0e79 100644
--- a/packages/SettingsLib/ActionButtonsPreference/Android.bp
+++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp
@@ -19,6 +19,7 @@
 
     static_libs: [
         "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
     ],
 
     sdk_version: "system_current",
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
new file mode 100644
index 0000000..3e73ebd
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/settingslib_expressive_space_extrasmall4"
+    android:paddingHorizontal="@dimen/settingslib_expressive_space_extrasmall4"
+    android:orientation="horizontal">
+
+    <LinearLayout
+        android:id="@+id/action1"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button1"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:id="@+id/text1"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action2"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button2"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:id="@+id/text2"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action3"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button3"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:id="@+id/text3"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action4"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button4"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:id="@+id/text4"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..cc948a6
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="SettingsLibActionButton.Expressive" parent="SettingsLibButtonStyle.Expressive.Tonal">
+        <item name="android:backgroundTint">@color/settingslib_materialColorPrimaryContainer</item>
+        <item name="iconTint">@color/settingslib_materialColorOnPrimaryContainer</item>
+        <item name="iconGravity">textTop</item>
+    </style>
+
+    <style name="SettingsLibActionButton.Expressive.Label" parent="SettingsLibTextAppearance.Emphasized.Title.Small">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_small3</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_small3</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSurface</item>
+        <item name="android:layout_gravity">center</item>
+    </style>
+
+</resources>
diff --git a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
index 5dc11cf..b286182 100644
--- a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
+++ b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
@@ -26,6 +26,8 @@
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.StringRes;
@@ -34,6 +36,8 @@
 
 import com.android.settingslib.widget.preference.actionbuttons.R;
 
+import com.google.android.material.button.MaterialButton;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -98,7 +102,10 @@
     }
 
     private void init() {
-        setLayoutResource(R.layout.settingslib_action_buttons);
+        int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+                ? R.layout.settingslib_expressive_action_buttons
+                : R.layout.settingslib_action_buttons;
+        setLayoutResource(resId);
         setSelectable(false);
 
         final Resources res = getContext().getResources();
@@ -127,6 +134,21 @@
         mButton3Info.mButton = (Button) holder.findViewById(R.id.button3);
         mButton4Info.mButton = (Button) holder.findViewById(R.id.button4);
 
+        if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            mButton1Info.mIsExpressive = true;
+            mButton1Info.mTextView = (TextView) holder.findViewById(R.id.text1);
+            mButton1Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action1);
+            mButton2Info.mIsExpressive = true;
+            mButton2Info.mTextView = (TextView) holder.findViewById(R.id.text2);
+            mButton2Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action2);
+            mButton3Info.mIsExpressive = true;
+            mButton3Info.mTextView = (TextView) holder.findViewById(R.id.text3);
+            mButton3Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action3);
+            mButton4Info.mIsExpressive = true;
+            mButton4Info.mTextView = (TextView) holder.findViewById(R.id.text4);
+            mButton4Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action4);
+        }
+
         mDivider1 = holder.findViewById(R.id.divider1);
         mDivider2 = holder.findViewById(R.id.divider2);
         mDivider3 = holder.findViewById(R.id.divider3);
@@ -169,45 +191,47 @@
             mVisibleButtonInfos.add(mButton4Info);
         }
 
-        final boolean isRtl = getContext().getResources().getConfiguration()
-                .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
-        switch (mVisibleButtonInfos.size()) {
-            case SINGLE_BUTTON_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
-                }
-                break;
-            case TWO_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
-                }
-                break;
-            case THREE_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
-                }
-                break;
-            case FOUR_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
-                }
-                break;
-            default:
-                Log.e(TAG, "No visible buttons info, skip background settings.");
-                break;
-        }
+        if (!SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            final boolean isRtl = getContext().getResources().getConfiguration()
+                    .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+            switch (mVisibleButtonInfos.size()) {
+                case SINGLE_BUTTON_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+                    }
+                    break;
+                case TWO_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+                    }
+                    break;
+                case THREE_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+                    }
+                    break;
+                case FOUR_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+                    }
+                    break;
+                default:
+                    Log.e(TAG, "No visible buttons info, skip background settings.");
+                    break;
+            }
 
-        setupDivider1();
-        setupDivider2();
-        setupDivider3();
+            setupDivider1();
+            setupDivider2();
+            setupDivider3();
+        }
     }
 
     private void setupBackgrounds(
@@ -509,23 +533,45 @@
 
     static class ButtonInfo {
         private Button mButton;
+        private TextView mTextView;
+        private LinearLayout mActionLayout;
         private CharSequence mText;
         private Drawable mIcon;
         private View.OnClickListener mListener;
         private boolean mIsEnabled = true;
         private boolean mIsVisible = true;
+        private boolean mIsExpressive = false;
 
         void setUpButton() {
-            mButton.setText(mText);
-            mButton.setOnClickListener(mListener);
-            mButton.setEnabled(mIsEnabled);
-            mButton.setCompoundDrawablesWithIntrinsicBounds(
-                    null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+            if (mIsExpressive) {
+                mTextView.setText(mText);
+                if (mButton instanceof MaterialButton) {
+                    ((MaterialButton) mButton).setIcon(mIcon);
+                }
+                mButton.setEnabled(mIsEnabled);
+                mActionLayout.setOnClickListener(mListener);
+                mActionLayout.setEnabled(mIsEnabled);
+                mActionLayout.setContentDescription(mText);
+            } else {
+                mButton.setText(mText);
+                mButton.setCompoundDrawablesWithIntrinsicBounds(
+                        null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+                mButton.setOnClickListener(mListener);
+                mButton.setEnabled(mIsEnabled);
+            }
 
             if (shouldBeVisible()) {
                 mButton.setVisibility(View.VISIBLE);
+                if (mIsExpressive) {
+                    mTextView.setVisibility(View.VISIBLE);
+                    mActionLayout.setVisibility(View.VISIBLE);
+                }
             } else {
                 mButton.setVisibility(View.GONE);
+                if (mIsExpressive) {
+                    mTextView.setVisibility(View.GONE);
+                    mActionLayout.setVisibility(View.GONE);
+                }
             }
         }
 
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0cb85d8..af07686 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -32,6 +32,8 @@
         "SettingsLibBannerMessagePreference",
         "SettingsLibBarChartPreference",
         "SettingsLibButtonPreference",
+        "SettingsLibBulletPreference",
+        "SettingsLibCardPreference",
         "SettingsLibCollapsingToolbarBaseActivity",
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibDisplayUtils",
@@ -53,6 +55,7 @@
         "SettingsLibTwoTargetPreference",
         "SettingsLibUsageProgressBarPreference",
         "SettingsLibUtils",
+        "SettingsLibZeroStatePreference",
         "settingslib_media_flags_lib",
         "settingslib_flags_lib",
     ],
diff --git a/packages/SettingsLib/BulletPreference/Android.bp b/packages/SettingsLib/BulletPreference/Android.bp
new file mode 100644
index 0000000..3ea0b2b
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SettingsLibBulletPreference",
+    use_resource_processor: true,
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+    ],
+}
diff --git a/packages/SettingsLib/BulletPreference/AndroidManifest.xml b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
new file mode 100644
index 0000000..c7495ef
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget.preference.bullet">
+
+    <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
new file mode 100644
index 0000000..030f024
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/icon_frame"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:minWidth="@dimen/settingslib_expressive_space_medium4"
+    android:gravity="top|start"
+    android:layout_marginTop="@dimen/settingslib_expressive_space_small1"
+    android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4"
+    android:paddingEnd="@dimen/settingslib_expressive_space_small1">
+
+    <androidx.preference.internal.PreferenceImageView
+        android:id="@android:id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:maxWidth="@dimen/settingslib_expressive_space_small4"
+        app:maxHeight="@dimen/settingslib_expressive_space_small4"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
new file mode 100644
index 0000000..3f37f6c
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false"
+    android:baselineAligned="false">
+
+    <include layout="@layout/settingslib_expressive_bullet_icon_frame"/>
+
+    <include layout="@layout/settingslib_expressive_preference_text_frame"/>
+
+    <!-- Preference should place its actual preference widget here. -->
+    <LinearLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="end|center_vertical"
+        android:paddingStart="@dimen/settingslib_expressive_space_small1"
+        android:paddingEnd="0dp"
+        android:orientation="vertical"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt
new file mode 100644
index 0000000..45e2e9a
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.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.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.settingslib.widget.preference.bullet.R
+
+/**
+ * The BulletPreference shows a text which describe a feature.
+ */
+class BulletPreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    init {
+        layoutResource = R.layout.settingslib_expressive_bullet_preference
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        holder.isDividerAllowedAbove = false
+        holder.isDividerAllowedBelow = false
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp
new file mode 100644
index 0000000..1d871d1
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SettingsLibCardPreference",
+    use_resource_processor: true,
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+    ],
+}
diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml
new file mode 100644
index 0000000..717f66e
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget.preference.card">
+
+    <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
new file mode 100644
index 0000000..716ed41
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<com.google.android.material.card.MaterialCardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/SettingsLibCardStyle">
+
+    <LinearLayout
+        android:id="@+id/card_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:baselineAligned="false"
+        android:minHeight="@dimen/settingslib_expressive_space_large3"
+        android:paddingStart="@dimen/settingslib_expressive_space_small1"
+        android:paddingEnd="@dimen/settingslib_expressive_space_small1"
+        android:orientation="horizontal"
+        android:gravity="center_vertical">
+
+        <LinearLayout
+            android:id="@+id/icon_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="@dimen/settingslib_expressive_space_medium3"
+            android:minHeight="@dimen/settingslib_expressive_space_medium3"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@android:id/icon"
+                android:src="@drawable/settingslib_arrow_drop_down"
+                android:layout_width="@dimen/settingslib_expressive_space_medium3"
+                android:layout_height="@dimen/settingslib_expressive_space_medium3"
+                android:scaleType="centerInside"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/text_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+            android:paddingVertical="@dimen/settingslib_expressive_space_small2"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@android:id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:hyphenationFrequency="normalFast"
+                android:lineBreakWordStyle="phrase"
+                android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/>
+
+            <TextView
+                android:id="@android:id/summary"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:hyphenationFrequency="normalFast"
+                android:lineBreakWordStyle="phrase"
+                android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/>
+
+        </LinearLayout>
+
+        <ImageView
+            android:id="@android:id/closeButton"
+            android:layout_width="@dimen/settingslib_expressive_space_medium4"
+            android:layout_height="@dimen/settingslib_expressive_space_medium4"
+            android:padding="@dimen/settingslib_expressive_space_extrasmall4"
+            android:layout_gravity="center"
+            android:src="@drawable/settingslib_expressive_icon_close"
+            android:background="?android:attr/selectableItemBackground" />
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
new file mode 100644
index 0000000..4cbdea5
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<resources>
+    <style name="TextAppearance.CardTitle.SettingsLib"
+        parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
+        <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+        <item name="android:textSize">20sp</item>
+    </style>
+
+    <style name="TextAppearance.CardSummary.SettingsLib"
+        parent="@style/TextAppearance.PreferenceSummary.SettingsLib">
+        <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item>
+        <item name="android:textSize">14sp</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
new file mode 100644
index 0000000..eb14746
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.card.R
+
+/**
+ * The CardPreference shows a card like suggestion in homepage, which also support dismiss.
+ */
+class CardPreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    init {
+        layoutResource = R.layout.settingslib_expressive_preference_card
+    }
+    private var dismissible = false
+        set(value) {
+            if (field != value) {
+                field = value
+                notifyChanged()
+            }
+        }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        holder.isDividerAllowedBelow = false
+        holder.isDividerAllowedAbove = false
+
+        holder.findViewById(android.R.id.closeButton)?.let { dismissButton ->
+            dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE
+            dismissButton.setOnClickListener {
+                isVisible = false
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
new file mode 100644
index 0000000..ccbe20e
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:start="16dp"
+        android:end="16dp"
+        android:top="4dp"
+        android:bottom="4dp">
+        <shape>
+            <size android:width="32dp" android:height="40dp" />
+            <solid android:color="@color/settingslib_materialColorSurfaceContainerHighest" />
+            <corners
+                android:radius="100dp" />
+        </shape>
+    </item>
+
+    <item
+        android:width="24dp"
+        android:height="24dp"
+        android:gravity="center"
+        android:start="16dp"
+        android:end="16dp"
+        android:top="4dp"
+        android:bottom="4dp">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportWidth="960"
+            android:viewportHeight="960"
+            android:tint="@color/settingslib_materialColorOnSurfaceVariant"
+            android:autoMirrored="true">
+            <path
+                android:fillColor="@android:color/white"
+                android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
new file mode 100644
index 0000000..b881c57
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/content_parent"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <include layout="@layout/settingslib_expressive_collapsing_toolbar_content_layout"/>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
new file mode 100644
index 0000000..6221659
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/app_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true"
+        android:outlineAmbientShadowColor="@android:color/transparent"
+        android:outlineSpotShadowColor="@android:color/transparent"
+        android:background="@android:color/transparent"
+        app:expanded="false"
+        android:theme="@style/SettingsLibTheme.CollapsingToolbar.Expressive">
+
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/settingslib_toolbar_layout_height"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+            app:toolbarId="@id/action_bar"
+            style="@style/SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+
+            <Toolbar
+                android:id="@+id/action_bar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                android:theme="?android:attr/actionBarTheme"
+                android:transitionName="shared_element_view"
+                app:layout_collapseMode="pin"/>
+
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <FrameLayout
+        android:id="@+id/content_frame"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+</merge>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..d58c2c2
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources>
+    <style name="SettingsLibCollapsingToolbarLayoutStyle">
+        <item name="expandedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Expanded</item>
+        <item name="collapsedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Collapsed</item>
+        <item name="expandedTitleMarginStart">@dimen/settingslib_expressive_space_small4</item>
+        <item name="expandedTitleMarginEnd">@dimen/settingslib_expressive_space_small4</item>
+        <item name="expandedTitleMarginBottom">@dimen/settingslib_expressive_space_medium1</item>
+        <item name="maxLines">3</item>
+        <item name="scrimVisibleHeightTrigger">@dimen/settingslib_scrim_visible_height_trigger</item>
+        <item name="contentScrim">@color/settingslib_materialColorSurfaceVariant</item>
+        <item name="statusBarScrim">@null</item>
+        <item name="scrimAnimationDuration">50</item>
+        <item name="collapsedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+        <item name="expandedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+    </style>
+    <style name="SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+        <item name="contentScrim">@color/settingslib_materialColorSurfaceContainer</item>
+    </style>
+
+    <style name="SettingsLibCollapsingToolbarTitle.Collapsed" parent="@android:style/TextAppearance.DeviceDefault.Headline">
+        <!--set dp because we don't want size adjust when font size change-->
+        <item name="android:textSize">20dp</item>
+    </style>
+
+    <style name="SettingsLibCollapsingToolbarTitle.Expanded" parent="CollapsingToolbarTitle.Collapsed">
+        <item name="android:textSize">36dp</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
new file mode 100644
index 0000000..ca1904a
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources>
+    <style name="SettingsLibTheme.CollapsingToolbar.Expressive" parent="@style/Theme.MaterialComponents.DayNight"/>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
index f46f110..feacecb 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
@@ -27,6 +27,8 @@
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
 
+import com.android.settingslib.widget.SettingsThemeHelper;
+
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
 import com.google.android.material.color.DynamicColors;
@@ -69,7 +71,10 @@
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             DynamicColors.applyToActivityIfAvailable(this);
         }
-        setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase);
+        int resId = SettingsThemeHelper.isExpressiveTheme(this)
+                ? com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive
+                : com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase;
+        setTheme(resId);
 
         if (mCustomizeLayoutResId > 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
             super.setContentView(mCustomizeLayoutResId);
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
index 16ed5a8..0f9d94e 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
@@ -28,6 +28,8 @@
 import androidx.annotation.Nullable;
 import androidx.fragment.app.FragmentActivity;
 
+import com.android.settingslib.widget.SettingsThemeHelper;
+
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
 
@@ -59,6 +61,11 @@
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         EdgeToEdgeUtils.enable(this);
         super.onCreate(savedInstanceState);
+
+        if (SettingsThemeHelper.isExpressiveTheme(this)) {
+            setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive);
+        }
+
         // for backward compatibility on R devices or wearable devices due to small device size.
         if (mCustomizeLayoutResId > 0 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.S
                 || isWatch())) {
@@ -66,7 +73,7 @@
             return;
         }
 
-        View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null);
+        View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null, this);
         super.setContentView(view);
     }
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 2ab2abd..01ecb66 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -20,6 +20,7 @@
 
 import android.app.ActionBar;
 import android.app.Activity;
+import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
@@ -37,6 +38,8 @@
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 
+import com.android.settingslib.widget.SettingsThemeHelper;
+
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
 
@@ -84,6 +87,8 @@
 
     private boolean mUseCollapsingToolbar;
 
+    private boolean mIsExpressiveTheme;
+
     public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback,
             boolean useCollapsingToolbar) {
         mHostCallback = hostCallback;
@@ -103,11 +108,16 @@
         int layoutId;
         boolean useCollapsingToolbar =
                 mUseCollapsingToolbar || Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
+        Context context = (activity != null) ? activity : inflater.getContext();
+        mIsExpressiveTheme = SettingsThemeHelper.isExpressiveTheme(context);
         if (useCollapsingToolbar) {
-            layoutId = R.layout.collapsing_toolbar_base_layout;
+            layoutId = mIsExpressiveTheme
+                    ? R.layout.settingslib_expressive_collapsing_toolbar_base_layout
+                    : R.layout.collapsing_toolbar_base_layout;
         } else {
             layoutId = R.layout.non_collapsing_toolbar_base_layout;
         }
+
         final View view = inflater.inflate(layoutId, container, false);
         if (view instanceof CoordinatorLayout) {
             mCoordinatorLayout = (CoordinatorLayout) view;
@@ -155,6 +165,9 @@
             if (actionBar != null) {
                 actionBar.setDisplayHomeAsUpEnabled(true);
                 actionBar.setHomeButtonEnabled(true);
+                if (mIsExpressiveTheme) {
+                    actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+                }
                 actionBar.setDisplayShowTitleEnabled(true);
             }
         }
@@ -174,6 +187,9 @@
         if (actionBar != null) {
             actionBar.setDisplayHomeAsUpEnabled(true);
             actionBar.setHomeButtonEnabled(true);
+            if (mIsExpressiveTheme) {
+                actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+            }
             actionBar.setDisplayShowTitleEnabled(true);
         }
     }
@@ -188,6 +204,9 @@
         if (actionBar != null) {
             actionBar.setDisplayHomeAsUpEnabled(true);
             actionBar.setHomeButtonEnabled(true);
+            if (mIsExpressiveTheme) {
+                actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+            }
             actionBar.setDisplayShowTitleEnabled(true);
         }
     }
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
index f70add9..51d7504 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
@@ -39,6 +39,7 @@
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 
 import com.android.settingslib.collapsingtoolbar.R;
+import com.android.settingslib.widget.SettingsThemeHelper;
 
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -105,7 +106,10 @@
     }
 
     private void init() {
-        inflate(getContext(), R.layout.collapsing_toolbar_content_layout, this);
+        int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+                ? R.layout.settingslib_expressive_collapsing_toolbar_content_layout
+                : R.layout.collapsing_toolbar_content_layout;
+        inflate(getContext(), resId, this);
         mCollapsingToolbarLayout = findViewById(R.id.collapsing_toolbar);
         mAppBarLayout = findViewById(R.id.app_bar);
         if (mCollapsingToolbarLayout != null) {
@@ -172,6 +176,9 @@
             actionBar.setDisplayHomeAsUpEnabled(true);
             actionBar.setHomeButtonEnabled(true);
             actionBar.setDisplayShowTitleEnabled(true);
+            if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+                actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+            }
         }
     }
 
@@ -202,6 +209,9 @@
             actionBar.setDisplayHomeAsUpEnabled(true);
             actionBar.setHomeButtonEnabled(true);
             actionBar.setDisplayShowTitleEnabled(true);
+            if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+                actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+            }
         }
     }
 
diff --git a/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
new file mode 100644
index 0000000..3999e76
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/settingslib_materialColorPrimaryContainer"/>
+            <corners android:radius="@dimen/settingslib_expressive_radius_extralarge3"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
new file mode 100644
index 0000000..4425ef0
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingVertical="@dimen/settingslib_expressive_space_small1"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/frame"
+        android:minHeight="@dimen/settingslib_expressive_space_large3"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:paddingStart="@dimen/settingslib_expressive_space_medium1"
+        android:paddingEnd="@dimen/settingslib_expressive_space_small3"
+        android:background="@drawable/settingslib_expressive_switch_bar_bg">
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4"
+            android:layout_marginVertical="@dimen/settingslib_expressive_space_small4"
+            android:layout_weight="1">
+
+            <TextView
+                android:id="@+id/switch_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start"
+                android:ellipsize="end"
+                android:textAlignment="viewStart"
+                android:textAppearance="?android:attr/textAppearanceListItem"
+                android:textColor="@color/settingslib_main_switch_text_color" />
+
+            <TextView
+                android:id="@+id/switch_summary"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignLeft="@id/switch_text"
+                android:layout_alignStart="@id/switch_text"
+                android:layout_below="@id/switch_text"
+                android:layout_gravity="start"
+                android:textAlignment="viewStart"
+                android:textAppearance="?android:attr/textAppearanceListItemSmall"
+                android:textColor="@color/settingslib_main_switch_text_color"
+                android:visibility="gone" />
+        </RelativeLayout>
+
+        <com.google.android.material.materialswitch.MaterialSwitch
+            android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+            android:id="@android:id/switch_widget"
+            style="@style/SettingslibMainSwitchStyle.Expressive" />
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index e6f61a8..106802e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -22,6 +22,7 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -31,6 +32,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.RequiresApi;
 
 import com.android.settingslib.widget.mainswitch.R;
 
@@ -52,6 +54,7 @@
     private int mBackgroundActivatedColor;
 
     protected TextView mTextView;
+    protected TextView mSummaryView;
     protected CompoundButton mSwitch;
     private final View mFrameView;
 
@@ -71,7 +74,11 @@
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
 
-        LayoutInflater.from(context).inflate(R.layout.settingslib_main_switch_bar, this);
+        boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context);
+        int resId = isExpressive
+                ? R.layout.settingslib_expressive_main_switch_bar
+                : R.layout.settingslib_main_switch_bar;
+        LayoutInflater.from(context).inflate(resId, this);
 
         if (Build.VERSION.SDK_INT < VERSION_CODES.S) {
             TypedArray a;
@@ -93,6 +100,9 @@
 
         mFrameView = findViewById(R.id.frame);
         mTextView = findViewById(R.id.switch_text);
+        if (isExpressive) {
+            mSummaryView = findViewById(R.id.switch_summary);
+        }
         mSwitch = findViewById(android.R.id.switch_widget);
         addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
 
@@ -109,6 +119,12 @@
             final CharSequence title = a.getText(
                     androidx.preference.R.styleable.Preference_android_title);
             setTitle(title);
+            //TODO(b/369470034): update to next version
+            if (isExpressive && Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
+                CharSequence summary = a.getText(
+                        androidx.preference.R.styleable.Preference_android_summary);
+                setSummary(summary);
+            }
             a.recycle();
         }
 
@@ -153,6 +169,18 @@
     }
 
     /**
+     * Set the summary text
+     */
+    @RequiresApi(VERSION_CODES.VANILLA_ICE_CREAM)
+    //TODO(b/369470034): update to next version
+    public void setSummary(CharSequence text) {
+        if (mSummaryView != null) {
+            mSummaryView.setText(text);
+            mSummaryView.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE);
+        }
+    }
+
+    /**
      * Set icon space reserved for title
      */
     public void setIconSpaceReserved(boolean iconSpaceReserved) {
@@ -219,6 +247,12 @@
             mFrameView.setEnabled(enabled);
             mFrameView.setActivated(isChecked());
         }
+
+        if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            if (mSummaryView != null) {
+                mSummaryView.setEnabled(enabled);
+            }
+        }
     }
 
     private void propagateChecked(boolean isChecked) {
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index b294d4e..d895c87 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
@@ -89,6 +90,10 @@
                     androidx.preference.R.styleable.Preference_android_title);
             setTitle(title);
 
+            CharSequence summary = a.getText(
+                    androidx.preference.R.styleable.Preference_android_summary);
+            setSummary(summary);
+
             final boolean bIconSpaceReserved = a.getBoolean(
                     androidx.preference.R.styleable.Preference_android_iconSpaceReserved, true);
             setIconSpaceReserved(bIconSpaceReserved);
@@ -113,6 +118,15 @@
     }
 
     @Override
+    public void setSummary(CharSequence summary) {
+        super.setSummary(summary);
+        if (mMainSwitchBar != null
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            mMainSwitchBar.setSummary(summary);
+        }
+    }
+
+    @Override
     public void setIconSpaceReserved(boolean iconSpaceReserved) {
         super.setIconSpaceReserved(iconSpaceReserved);
         if (mMainSwitchBar != null) {
diff --git a/packages/SettingsLib/Service/Android.bp b/packages/SettingsLib/Service/Android.bp
new file mode 100644
index 0000000..65d0e4c
--- /dev/null
+++ b/packages/SettingsLib/Service/Android.bp
@@ -0,0 +1,20 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "SettingsLibService-srcs",
+    srcs: ["src/**/*.kt"],
+}
+
+android_library {
+    name: "SettingsLibService",
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+    srcs: [":SettingsLibService-srcs"],
+    static_libs: [
+        "SettingsLibGraph",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Service/AndroidManifest.xml b/packages/SettingsLib/Service/AndroidManifest.xml
new file mode 100644
index 0000000..56d7818
--- /dev/null
+++ b/packages/SettingsLib/Service/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.service">
+
+  <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
new file mode 100644
index 0000000..95661c9
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.service
+
+import android.app.Application
+import com.android.settingslib.graph.GetPreferenceGraphApiHandler
+import com.android.settingslib.graph.GetPreferenceGraphRequest
+
+/** Api to get preference graph. */
+internal class PreferenceGraphApi(activityClasses: Set<String>) :
+    GetPreferenceGraphApiHandler(activityClasses) {
+    override val id: Int
+        get() = API_GET_PREFERENCE_GRAPH
+
+    override fun hasPermission(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: GetPreferenceGraphRequest,
+    ): Boolean {
+        return true // TODO: add permission check
+    }
+}
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
new file mode 100644
index 0000000..d382dad
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.service
+
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.MessengerService
+import com.android.settingslib.ipc.PermissionChecker
+
+/**
+ * Preference service providing a bunch of APIs.
+ *
+ * In AndroidManifest.xml, the <service> must specify <intent-filter> with action
+ * [PREFERENCE_SERVICE_ACTION].
+ */
+open class PreferenceService(
+    permissionChecker: PermissionChecker,
+    name: String = "PreferenceService",
+) :
+    MessengerService(
+        listOf<ApiHandler<*, *>>(PreferenceGraphApi(setOf())),
+        permissionChecker,
+        name,
+    )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
new file mode 100644
index 0000000..8f03111
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.service
+
+const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
+
+internal const val API_GET_PREFERENCE_GRAPH = 1
diff --git a/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
new file mode 100644
index 0000000..a6885a4
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- State Disabled, Unchecked -->
+    <item
+        android:alpha="?android:attr/disabledAlpha"
+        android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+        android:state_enabled="false" android:state_checked="false"/>
+    <!-- State Disabled, Checked -->
+    <item
+        android:alpha="?android:attr/disabledAlpha"
+        android:color="@color/settingslib_materialColorPrimary"
+        android:state_enabled="false" android:state_checked="true"/>
+    <!-- State Checked -->
+    <item
+        android:color="@color/settingslib_materialColorPrimary"
+        android:state_checked="true"/>
+
+    <!-- State Unchecked -->
+    <item
+        android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+        android:state_hovered="true" android:state_checked="false"/>
+    <item
+        android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+        android:state_focused="true" android:state_checked="false"/>
+    <item
+        android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+        android:state_pressed="true" android:state_checked="false"/>
+    <item
+        android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+        android:state_checked="false"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml
new file mode 100644
index 0000000..161ece7
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <size android:width="24dp" android:height="24dp"/>
+            <solid android:color="@color/settingslib_materialColorSurfaceDim"/>
+        </shape>
+    </item>
+    <item>
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportWidth="960"
+            android:viewportHeight="960">
+            <path
+                android:fillColor="@color/settingslib_materialColorOnSurface"
+                android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/>
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml
new file mode 100644
index 0000000..1b5d518
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <size android:width="24dp" android:height="24dp"/>
+            <solid android:color="@color/settingslib_materialColorSurfaceDim"/>
+        </shape>
+    </item>
+    <item>
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportWidth="960"
+            android:viewportHeight="960">
+            <path
+                android:fillColor="@color/settingslib_materialColorOnSurface"
+                android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
new file mode 100644
index 0000000..9216c96
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+<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="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
index dda7517c..952562e 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
@@ -31,37 +31,7 @@
 
     <include layout="@layout/settingslib_icon_frame"/>
 
-    <RelativeLayout
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:paddingTop="16dp"
-        android:paddingBottom="16dp">
-
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start"
-            android:textAlignment="viewStart"
-            android:maxLines="2"
-            android:textAppearance="?android:attr/textAppearanceListItem"
-            android:ellipsize="marquee"/>
-
-        <TextView
-            android:id="@android:id/summary"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_below="@android:id/title"
-            android:layout_alignLeft="@android:id/title"
-            android:layout_alignStart="@android:id/title"
-            android:layout_gravity="start"
-            android:textAlignment="viewStart"
-            android:textColor="?android:attr/textColorSecondary"
-            android:maxLines="10"
-            style="@style/PreferenceSummaryTextStyle"/>
-
-    </RelativeLayout>
+    <include layout="@layout/settingslib_preference_frame"/>
 
     <!-- Preference should place its actual preference widget here. -->
     <LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
new file mode 100644
index 0000000..433d264
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dp"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:paddingTop="16dp"
+    android:paddingBottom="16dp">
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        android:text="Title"
+        android:maxLines="2"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:ellipsize="marquee"/>
+
+    <TextView
+        android:id="@android:id/summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@android:id/title"
+        android:layout_alignLeft="@android:id/title"
+        android:layout_alignStart="@android:id/title"
+        android:text="Summary summary summary"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        android:textColor="?android:attr/textColorSecondary"
+        android:maxLines="10"
+        style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
index fedcc77..4e23b65 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
@@ -31,41 +31,7 @@
 
     <include layout="@layout/settingslib_icon_frame"/>
 
-    <RelativeLayout
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:paddingTop="16dp"
-        android:paddingBottom="16dp">
-
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start"
-            android:textAlignment="viewStart"
-            android:maxLines="2"
-            android:hyphenationFrequency="normalFast"
-            android:lineBreakWordStyle="phrase"
-            android:textAppearance="?android:attr/textAppearanceListItem"
-            android:ellipsize="marquee"/>
-
-        <TextView
-            android:id="@android:id/summary"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_below="@android:id/title"
-            android:layout_alignLeft="@android:id/title"
-            android:layout_alignStart="@android:id/title"
-            android:layout_gravity="start"
-            android:textAlignment="viewStart"
-            android:textColor="?android:attr/textColorSecondary"
-            android:maxLines="10"
-            android:hyphenationFrequency="normalFast"
-            android:lineBreakWordStyle="phrase"
-            style="@style/PreferenceSummaryTextStyle"/>
-
-    </RelativeLayout>
+    <include layout="@layout/settingslib_preference_frame"/>
 
     <!-- Preference should place its actual preference widget here. -->
     <LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
new file mode 100644
index 0000000..f93e1b9
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dp"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:paddingTop="16dp"
+    android:paddingBottom="16dp">
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        android:text="Title"
+        android:maxLines="2"
+        android:hyphenationFrequency="normalFast"
+        android:lineBreakWordStyle="phrase"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:ellipsize="marquee"/>
+
+    <TextView
+        android:id="@android:id/summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@android:id/title"
+        android:layout_alignLeft="@android:id/title"
+        android:layout_alignStart="@android:id/title"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        android:textColor="?android:attr/textColorSecondary"
+        android:maxLines="10"
+        android:text="Summary summary summary"
+        android:hyphenationFrequency="normalFast"
+        android:lineBreakWordStyle="phrase"
+        style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml
new file mode 100644
index 0000000..245d368
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="@dimen/settingslib_expressive_space_small1"
+    android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4"
+    android:orientation="vertical"
+    android:animateLayoutChanges="true"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false">
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:textAlignment="viewStart"
+        android:clickable="false"
+        android:longClickable="false"
+        android:maxLines="10"
+        android:ellipsize="end"
+        android:textAppearance="@style/TextAppearance.TopIntroText"/>
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/collapse_button"
+        app:layout_constraintTop_toBottomOf="@android:id/title"
+        app:layout_constraintStart_toStartOf="parent"
+        android:text="@string/settingslib_expressive_text_expand"
+        app:icon="@drawable/settingslib_expressive_icon_expand"
+        style="@style/SettingslibTextButtonStyle.Expressive"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml
new file mode 100644
index 0000000..857dd79
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <declare-styleable name="CollapsableTextView">
+        <attr name="android:gravity"/>
+        <!-- The minimum number of lines when the textView collapsed. -->
+        <attr name="android:minLines"/>
+        <!-- Specifies that the textView is collapsable. -->
+        <attr name="isCollapsable" format="boolean"/>
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
index 2320aab..0542c51 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
@@ -48,6 +48,7 @@
     <dimen name="settingslib_expressive_space_medium2">36dp</dimen>
     <dimen name="settingslib_expressive_space_medium3">40dp</dimen>
     <dimen name="settingslib_expressive_space_medium4">48dp</dimen>
+    <dimen name="settingslib_expressive_space_medium5">56dp</dimen>
     <dimen name="settingslib_expressive_space_large1">60dp</dimen>
     <dimen name="settingslib_expressive_space_large2">64dp</dimen>
     <dimen name="settingslib_expressive_space_large3">72dp</dimen>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml
new file mode 100644
index 0000000..2273406
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- text of button to indicate user the textView is expandable [CHAR LIMIT=NONE] -->
+    <string name="settingslib_expressive_text_expand">Expand</string>
+    <!-- text of button to indicate user the textView is collapsable [CHAR LIMIT=NONE] -->
+    <string name="settingslib_expressive_text_collapse">Collapse</string>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 04ae80e..250c27e 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -169,4 +169,125 @@
         <item name="android:focusable">false</item>
         <item name="thumbIcon">@drawable/settingslib_expressive_switch_thumb_icon</item>
     </style>
+
+    <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive">
+        <item name="android:layout_gravity">center</item>
+        <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
+    </style>
+
+    <style name="SettingsLibCardStyle" parent="">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item>
+        <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item>
+        <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item>
+        <item name="cardElevation">0dp</item>
+        <item name="rippleColor">?android:attr/colorControlHighlight</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled"
+        parent="@style/Widget.Material3.Button">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:backgroundTint">@color/settingslib_materialColorPrimary</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconTint">@color/settingslib_materialColorOnPrimary</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Filled.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Tonal"
+        parent="@style/Widget.Material3.Button.TonalButton">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:backgroundTint">@color/settingslib_materialColorSecondaryContainer</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSecondaryContainer</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconTint">@color/settingslib_materialColorOnSecondaryContainer</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Tonal.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Tonal.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Tonal.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline"
+        parent="@style/Widget.Material3.Button.OutlinedButton.Icon">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorPrimary</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconTint">@color/settingslib_materialColorPrimary</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+        <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="strokeColor">@color/settingslib_materialColorOutlineVariant</item>
+
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Outline.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
+    <style name="SettingslibTextButtonStyle.Expressive"
+        parent="@style/Widget.Material3.Button.TextButton.Icon">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSurface</item>
+        <item name="iconTint">@null</item>
+        <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="rippleColor">?android:attr/colorControlHighlight</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
new file mode 100644
index 0000000..127f21a
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.settingslib.widget.theme.R
+import com.google.android.material.button.MaterialButton
+
+class CollapsableTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    private var isCollapsable: Boolean = false
+    private var isCollapsed: Boolean = false
+    private var minLines: Int = DEFAULT_MIN_LINES
+
+    private val titleTextView: TextView
+    private val collapseButton: MaterialButton
+    private val collapseButtonResources: CollapseButtonResources
+
+    init {
+        LayoutInflater.from(context)
+            .inflate(R.layout.settingslib_expressive_collapsable_textview, this)
+        titleTextView = findViewById(android.R.id.title)
+        collapseButton = findViewById(R.id.collapse_button)
+
+        collapseButtonResources = CollapseButtonResources(
+            context.getDrawable(R.drawable.settingslib_expressive_icon_collapse)!!,
+            context.getDrawable(R.drawable.settingslib_expressive_icon_expand)!!,
+            context.getString(R.string.settingslib_expressive_text_collapse),
+            context.getString(R.string.settingslib_expressive_text_expand)
+        )
+
+        collapseButton.setOnClickListener {
+            isCollapsed = !isCollapsed
+            updateView()
+        }
+
+        initAttributes(context, attrs, defStyleAttr)
+    }
+
+    private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
+        context.obtainStyledAttributes(
+            attrs, Attrs, defStyleAttr, 0
+        ).apply {
+            val gravity = getInt(GravityAttr, Gravity.START)
+            when (gravity) {
+                Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> {
+                    centerHorizontally(titleTextView)
+                    centerHorizontally(collapseButton)
+                }
+            }
+            recycle()
+        }
+    }
+
+    private fun centerHorizontally(view: View) {
+        (view.layoutParams as LayoutParams).apply {
+            startToStart = LayoutParams.PARENT_ID
+            endToEnd = LayoutParams.PARENT_ID
+            horizontalBias = 0.5f
+        }
+    }
+
+    /**
+     * Sets the text content of the CollapsableTextView.
+     * @param text The text to display.
+     */
+    fun setText(text: String) {
+        titleTextView.text = text
+    }
+
+    /**
+     * Sets whether the text view is collapsable.
+     * @param collapsable True if the text view should be collapsable, false otherwise.
+     */
+    fun setCollapsable(collapsable: Boolean) {
+        isCollapsable = collapsable
+        updateView()
+    }
+
+    /**
+     * Sets the minimum number of lines to display when collapsed.
+     * @param lines The minimum number of lines.
+     */
+    fun setMinLines(line: Int) {
+        minLines = line.coerceIn(1, DEFAULT_MAX_LINES)
+        updateView()
+    }
+
+    private fun updateView() {
+        when {
+            isCollapsed -> {
+                collapseButton.apply {
+                    text = collapseButtonResources.expandText
+                    icon = collapseButtonResources.expandIcon
+                }
+                titleTextView.maxLines = minLines
+            }
+
+            else -> {
+                collapseButton.apply {
+                    text = collapseButtonResources.collapseText
+                    icon = collapseButtonResources.collapseIcon
+                }
+                titleTextView.maxLines = DEFAULT_MAX_LINES
+            }
+        }
+        collapseButton.visibility = if (isCollapsable) VISIBLE else GONE
+    }
+
+    private data class CollapseButtonResources(
+        val collapseIcon: Drawable,
+        val expandIcon: Drawable,
+        val collapseText: String,
+        val expandText: String
+    )
+
+    companion object {
+        private const val DEFAULT_MAX_LINES = 10
+        private const val DEFAULT_MIN_LINES = 2
+
+        private val Attrs = R.styleable.CollapsableTextView
+        private val GravityAttr = R.styleable.CollapsableTextView_android_gravity
+    }
+}
+
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index d01c0b9..272dc2d 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
 #
 
 [versions]
-agp = "8.6.0"
+agp = "8.6.1"
 compose-compiler = "1.5.11"
 dexmaker-mockito = "2.28.3"
 jvm = "17"
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
deleted file mode 100644
index 50432f3..0000000
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
new file mode 100644
index 0000000..45f0424
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
index 9a7f4b6..1c25e974 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -16,6 +16,6 @@
 
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=gradle-8.10-bin.zip
+distributionUrl=gradle-8.10.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 790aa9f..8f8275b 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -58,12 +58,12 @@
     api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
     api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
     api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
+    api("androidx.graphics:graphics-shapes-android:1.0.1")
     api("androidx.lifecycle:lifecycle-livedata-ktx")
     api("androidx.lifecycle:lifecycle-runtime-compose")
     api("androidx.navigation:navigation-compose:2.8.1")
     api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
-    api("com.google.android.material:material:1.11.0")
-    api("androidx.graphics:graphics-shapes-android:1.0.1")
+    api("com.google.android.material:material:1.12.0")
     debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
     implementation("com.airbnb.android:lottie-compose:6.4.0")
 
@@ -84,7 +84,6 @@
 
                     // Excludes files forked from AndroidX.
                     "com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
-                    "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
 
                     // Excludes files forked from Accompanist.
                     "com/android/settingslib/spa/framework/compose/DrawablePainter*",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 460bf99..965c971 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -162,3 +162,6 @@
 /** Creates a new [TextStyle] which font weight set to medium. */
 internal fun TextStyle.toMediumWeight() =
     copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em)
+
+internal fun TextStyle.toSemiBoldWeight() =
+    copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 7e1df16..5bb57b8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -110,7 +110,7 @@
                 shape = RectangleShape,
                 colors = ButtonDefaults.filledTonalButtonColors(
                     containerColor = MaterialTheme.colorScheme.primaryContainer,
-                    contentColor = MaterialTheme.colorScheme.onPrimary,
+                    contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
                     disabledContainerColor = MaterialTheme.colorScheme.surface,
                 ),
                 contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 94d2c21..f99d206 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -41,9 +41,7 @@
 internal fun NavigateBack() {
     val navController = LocalNavController.current
     val contentDescription = stringResource(R.string.abc_action_bar_up_description)
-    BackAction(contentDescription) {
-        navController.navigateBack()
-    }
+    BackAction(contentDescription) { navController.navigateBack() }
 }
 
 /** Action that collapses the search bar. */
@@ -55,15 +53,35 @@
 
 @Composable
 private fun BackAction(contentDescription: String, onClick: () -> Unit) {
-    IconButton(onClick) {
+    IconButton(
+        onClick = onClick,
+        modifier =
+        if (isSpaExpressiveEnabled)
+            Modifier
+                .padding(
+                    start = SettingsDimension.paddingLarge,
+                    end = SettingsDimension.paddingSmall,
+                    top = SettingsDimension.paddingExtraSmall,
+                    bottom = SettingsDimension.paddingExtraSmall,
+                )
+                .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
+                .clip(SettingsShape.CornerExtraLarge)
+        else Modifier,
+    ) {
         Icon(
             imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
             contentDescription = contentDescription,
-            modifier = if (isSpaExpressiveEnabled) Modifier
-                .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
-                .clip(SettingsShape.CornerExtraLarge)
-                .background(MaterialTheme.colorScheme.surfaceContainerHigh)
-                .padding(SettingsDimension.actionIconPadding) else Modifier
+            modifier =
+            if (isSpaExpressiveEnabled)
+                Modifier
+                    .size(
+                        SettingsDimension.actionIconWidth,
+                        SettingsDimension.actionIconHeight,
+                    )
+                    .clip(SettingsShape.CornerExtraLarge)
+                    .background(MaterialTheme.colorScheme.surfaceContainerHighest)
+                    .padding(SettingsDimension.actionIconPadding)
+            else Modifier,
         )
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 2ae3b56..2c55779 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -78,7 +78,9 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
 import com.android.settingslib.spa.framework.theme.settingsBackground
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.roundToInt
@@ -116,8 +118,12 @@
 ) {
     TwoRowsTopAppBar(
         title = { Title(title = title, maxLines = 3) },
-        titleTextStyle = MaterialTheme.typography.displaySmall,
-        smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+        titleTextStyle =
+            if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight()
+            else MaterialTheme.typography.displaySmall,
+        smallTitleTextStyle =
+            if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight()
+            else MaterialTheme.typography.titleLarge,
         titleBottomPadding = LargeTitleBottomPadding,
         smallTitle = { Title(title = title, maxLines = 1) },
         modifier = modifier,
@@ -136,7 +142,9 @@
         text = title,
         modifier =
             Modifier.padding(
-                    start = SettingsDimension.itemPaddingAround,
+                    start =
+                        if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall
+                        else SettingsDimension.itemPaddingAround,
                     end = SettingsDimension.itemPaddingEnd,
                 )
                 .semantics { heading() },
@@ -194,7 +202,7 @@
         return lerp(
             containerColor,
             scrolledContainerColor,
-            FastOutLinearInEasing.transform(colorTransitionFraction)
+            FastOutLinearInEasing.transform(colorTransitionFraction),
         )
     }
 
@@ -241,7 +249,7 @@
             Row(
                 horizontalArrangement = Arrangement.End,
                 verticalAlignment = Alignment.CenterVertically,
-                content = actions
+                content = actions,
             )
         }
 
@@ -296,7 +304,7 @@
     windowInsets: WindowInsets,
     colors: TopAppBarColors,
     pinnedHeight: Dp,
-    scrollBehavior: TopAppBarScrollBehavior?
+    scrollBehavior: TopAppBarScrollBehavior?,
 ) {
     if (MaxHeightWithoutTitle <= pinnedHeight) {
         throw IllegalArgumentException(
@@ -333,7 +341,7 @@
             Row(
                 horizontalArrangement = Arrangement.End,
                 verticalAlignment = Alignment.CenterVertically,
-                content = actions
+                content = actions,
             )
         }
     val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
@@ -356,9 +364,9 @@
                         scrollBehavior.state,
                         velocity,
                         scrollBehavior.flingAnimationSpec,
-                        scrollBehavior.snapAnimationSpec
+                        scrollBehavior.snapAnimationSpec,
                     )
-                }
+                },
             )
         } else {
             Modifier
@@ -412,7 +420,8 @@
                                 val measuredMaxHeightPx =
                                     density.run {
                                         MaxHeightWithoutTitle.toPx() +
-                                            coordinates.size.height.toFloat()
+                                            coordinates.size.height.toFloat() +
+                                            titleBaselineHeight.toPx()
                                     }
                                 // Allow larger max height for multi-line title, but do not reduce
                                 // max height to prevent flaky.
@@ -430,7 +439,7 @@
                 titleBottomPadding = titleBottomPaddingPx,
                 hideTitleSemantics = hideBottomRowSemantics,
                 navigationIcon = {},
-                actions = {}
+                actions = {},
             )
         }
     }
@@ -485,7 +494,7 @@
             Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) {
                 CompositionLocalProvider(
                     LocalContentColor provides navigationIconContentColor,
-                    content = navigationIcon
+                    content = navigationIcon,
                 )
             }
             Box(
@@ -504,18 +513,18 @@
                                     fontScale = if (titleScaleDisabled) 1f else fontScale,
                                 )
                             },
-                        content = title
+                        content = title,
                     )
                 }
             }
             Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
                 CompositionLocalProvider(
                     LocalContentColor provides actionIconContentColor,
-                    content = actions
+                    content = actions,
                 )
             }
         },
-        modifier = modifier
+        modifier = modifier,
     ) { measurables, constraints ->
         val navigationIconPlaceable =
             measurables
@@ -552,7 +561,7 @@
             // Navigation icon
             navigationIconPlaceable.placeRelative(
                 x = 0,
-                y = (layoutHeight - navigationIconPlaceable.height) / 2
+                y = (layoutHeight - navigationIconPlaceable.height) / 2,
             )
 
             // Title
@@ -570,17 +579,17 @@
                                     titlePlaceable.height -
                                     max(
                                         0,
-                                        titleBottomPadding - titlePlaceable.height + titleBaseline
+                                        titleBottomPadding - titlePlaceable.height + titleBaseline,
                                     )
                         // Arrangement.Top
                         else -> 0
-                    }
+                    },
             )
 
             // Action icons
             actionIconsPlaceable.placeRelative(
                 x = constraints.maxWidth - actionIconsPlaceable.width,
-                y = (layoutHeight - actionIconsPlaceable.height) / 2
+                y = (layoutHeight - actionIconsPlaceable.height) / 2,
             )
         }
     }
@@ -595,7 +604,7 @@
     state: TopAppBarState,
     velocity: Float,
     flingAnimationSpec: DecayAnimationSpec<Float>?,
-    snapAnimationSpec: AnimationSpec<Float>?
+    snapAnimationSpec: AnimationSpec<Float>?,
 ): Velocity {
     // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
     // and just return Zero Velocity.
@@ -609,20 +618,18 @@
     // continue the motion to expand or collapse the app bar.
     if (flingAnimationSpec != null && abs(velocity) > 1f) {
         var lastValue = 0f
-        AnimationState(
-                initialValue = 0f,
-                initialVelocity = velocity,
-            )
-            .animateDecay(flingAnimationSpec) {
-                val delta = value - lastValue
-                val initialHeightOffset = state.heightOffset
-                state.heightOffset = initialHeightOffset + delta
-                val consumed = abs(initialHeightOffset - state.heightOffset)
-                lastValue = value
-                remainingVelocity = this.velocity
-                // avoid rounding errors and stop if anything is unconsumed
-                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
-            }
+        AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
+            flingAnimationSpec
+        ) {
+            val delta = value - lastValue
+            val initialHeightOffset = state.heightOffset
+            state.heightOffset = initialHeightOffset + delta
+            val consumed = abs(initialHeightOffset - state.heightOffset)
+            lastValue = value
+            remainingVelocity = this.velocity
+            // avoid rounding errors and stop if anything is unconsumed
+            if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+        }
     }
     // Snap if animation specs were provided.
     if (snapAnimationSpec != null) {
@@ -633,7 +640,7 @@
                 } else {
                     state.heightOffsetLimit
                 },
-                animationSpec = snapAnimationSpec
+                animationSpec = snapAnimationSpec,
             ) {
                 state.heightOffset = value
             }
@@ -647,9 +654,10 @@
 // Medium or Large app bar.
 private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
 
-internal val MaxHeightWithoutTitle = 124.dp
+internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp
 internal val DefaultTitleHeight = 52.dp
 internal val ContainerHeight = 56.dp
+private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp
 private val LargeTitleBottomPadding = 28.dp
 private val TopAppBarHorizontalPadding = 4.dp
 
diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp
index e70201b..76e36dc 100644
--- a/packages/SettingsLib/TopIntroPreference/Android.bp
+++ b/packages/SettingsLib/TopIntroPreference/Android.bp
@@ -14,7 +14,10 @@
         "SettingsLintDefaults",
     ],
 
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     resource_dirs: ["res"],
 
     static_libs: [
diff --git a/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml
new file mode 100644
index 0000000..fb13ef7
--- /dev/null
+++ b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <com.android.settingslib.widget.CollapsableTextView
+        android:id="@+id/collapsable_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java
deleted file mode 100644
index 1bbd76d..0000000
--- a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settingslib.widget.preference.topintro.R;
-
-/**
- * The TopIntroPreference shows a text which describe a feature. Gernerally, we expect this
- * preference always shows on the top of screen.
- */
-public class TopIntroPreference extends Preference {
-
-    public TopIntroPreference(Context context) {
-        super(context);
-        setLayoutResource(R.layout.top_intro_preference);
-        setSelectable(false);
-    }
-
-    public TopIntroPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setLayoutResource(R.layout.top_intro_preference);
-        setSelectable(false);
-    }
-
-    @Override
-    public void onBindViewHolder(PreferenceViewHolder holder) {
-        super.onBindViewHolder(holder);
-        holder.setDividerAllowedAbove(false);
-        holder.setDividerAllowedBelow(false);
-    }
-}
diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
new file mode 100644
index 0000000..afced0c
--- /dev/null
+++ b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.widget
+
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import androidx.annotation.RequiresApi
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.topintro.R
+
+open class TopIntroPreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    private var isCollapsable: Boolean = false
+    private var minLines: Int = 2
+
+    init {
+        if (SettingsThemeHelper.isExpressiveTheme(context)) {
+            layoutResource = R.layout.settingslib_expressive_top_intro
+            initAttributes(context, attrs, defStyleAttr)
+        } else {
+            layoutResource = R.layout.top_intro_preference
+        }
+        isSelectable = false
+    }
+
+    private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
+        context.obtainStyledAttributes(
+            attrs,
+            COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0
+        ).apply {
+            isCollapsable = getBoolean(IS_COLLAPSABLE, false)
+            minLines = getInt(
+                MIN_LINES,
+                if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
+            ).coerceIn(1, DEFAULT_MAX_LINES)
+            recycle()
+        }
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        holder.isDividerAllowedAbove = false
+        holder.isDividerAllowedBelow = false
+
+        if (!SettingsThemeHelper.isExpressiveTheme(context)) {
+            return
+        }
+
+        (holder.findViewById(R.id.collapsable_text_view) as? CollapsableTextView)?.apply {
+            setCollapsable(isCollapsable)
+            setMinLines(minLines)
+            setText(title.toString())
+        }
+    }
+
+    /**
+     * Sets whether the text view is collapsable.
+     * @param collapsable True if the text view should be collapsable, false otherwise.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    fun setCollapsable(collapsable: Boolean) {
+        isCollapsable = collapsable
+        notifyChanged()
+    }
+
+    /**
+     * Sets the minimum number of lines to display when collapsed.
+     * @param lines The minimum number of lines.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    fun setMinLines(lines: Int) {
+        minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
+        notifyChanged()
+    }
+
+    companion object {
+        private const val DEFAULT_MAX_LINES = 10
+        private const val DEFAULT_MIN_LINES = 2
+
+        private val COLLAPSABLE_TEXT_VIEW_ATTRS =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView
+        private val MIN_LINES =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines
+        private val IS_COLLAPSABLE =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable
+    }
+}
diff --git a/packages/SettingsLib/ZeroStatePreference/Android.bp b/packages/SettingsLib/ZeroStatePreference/Android.bp
new file mode 100644
index 0000000..4fc00bd
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SettingsLibZeroStatePreference",
+    use_resource_processor: true,
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "28",
+    apex_available: [
+        "//apex_available:platform",
+    ],
+}
diff --git a/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml
new file mode 100644
index 0000000..51b0ab8
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget.preference.zerostate">
+
+    <uses-sdk android:minSdkVersion="28" />
+
+</manifest>
diff --git a/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml
new file mode 100644
index 0000000..f42b441
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="160dp"
+    android:height="160dp"
+    android:viewportWidth="161"
+    android:viewportHeight="161">
+    <path
+        android:pathData="M67.2,4.43C74.79,-1.41 85.35,-1.41 92.94,4.43L112.01,19.1C113.48,20.23 115.09,21.16 116.8,21.87L139.02,31.08C147.86,34.74 153.14,43.9 151.89,53.4L148.74,77.28C148.5,79.12 148.5,80.98 148.74,82.82L151.89,106.71C153.14,116.2 147.86,125.36 139.02,129.03L116.8,138.23C115.09,138.95 113.48,139.87 112.01,141L92.94,155.67C85.35,161.51 74.79,161.51 67.2,155.67L48.13,141C46.66,139.87 45.05,138.95 43.34,138.23L21.12,129.03C12.28,125.36 7,116.2 8.25,106.71L11.4,82.82C11.64,80.98 11.64,79.12 11.4,77.28L8.25,53.4C7,43.9 12.28,34.74 21.12,31.08L43.34,21.87C45.05,21.16 46.66,20.23 48.13,19.1L67.2,4.43Z"
+        android:fillColor="@color/settingslib_materialColorSurfaceContainerHigh"/>
+</vector>
diff --git a/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml
new file mode 100644
index 0000000..c0b195c
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:ignore="ContentDescription">
+
+        <ImageView
+            android:layout_width="@dimen/settingslib_expressive_zero_state_background_size"
+            android:layout_height="@dimen/settingslib_expressive_zero_state_background_size"
+            android:src="@drawable/settingslib_expressive_zerostate_background"/>
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_large3"
+            android:layout_gravity="center"/>
+
+    </FrameLayout>
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="@dimen/settingslib_expressive_zero_state_title_width"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:layout_marginTop="@dimen/settingslib_expressive_space_small4"/>
+    <TextView
+        android:id="@android:id/summary"
+        android:layout_width="@dimen/settingslib_expressive_zero_state_title_width"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:layout_marginTop="@dimen/settingslib_expressive_space_small4"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml b/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml
new file mode 100644
index 0000000..e981ecc
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <dimen name="settingslib_expressive_zero_state_background_size">160dp</dimen>
+    <dimen name="settingslib_expressive_zero_state_title_width">316dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt
new file mode 100644
index 0000000..9b1ccef
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.widget
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.settingslib.widget.preference.zerostate.R
+
+class ZeroStatePreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val iconTint: Int = context.getColor(
+        com.android.settingslib.widget.theme.R.color.settingslib_materialColorOnSecondaryContainer
+    )
+    private var tintedIcon: Drawable? = null
+
+    init {
+        isSelectable = false
+        layoutResource = R.layout.settingslib_expressive_preference_zerostate
+        icon?.let { originalIcon ->
+            tintedIcon = originalIcon.mutate().apply {
+                colorFilter = PorterDuffColorFilter(iconTint, PorterDuff.Mode.SRC_IN)
+            }
+        }
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+
+        holder.itemView.isFocusable = false
+        holder.itemView.isClickable = false
+
+        (holder.findViewById(android.R.id.icon) as? ImageView)?.apply {
+            setImageDrawable(tintedIcon ?: icon)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 34e33c0..efc98db 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1645,13 +1645,13 @@
     <string name="media_transfer_headphone_name">Headphone</string>
 
     <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] -->
-    <string name="media_transfer_usb_speaker_name">USB speaker</string>
+    <string name="media_transfer_usb_audio_name">USB audio</string>
 
     <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] -->
     <string name="media_transfer_wired_device_mic_name">Mic jack</string>
 
     <!-- Name of the usb audio device mic. [CHAR LIMIT=50] -->
-    <string name="media_transfer_usb_device_mic_name">USB mic</string>
+    <string name="media_transfer_usb_device_mic_name">USB microphone</string>
 
     <!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] -->
     <string name="wifi_hotspot_switch_on_text">On</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
index 58dc8c7..e7c7476 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
@@ -45,6 +45,7 @@
             DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
             DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER,
             DeviceSettingId.DEVICE_SETTING_ID_ANC,
+            DeviceSettingId.DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER,
         },
         open = true)
 public @interface DeviceSettingId {
@@ -114,6 +115,9 @@
     /** Device setting ID for "More Settings" page. */
     int DEVICE_SETTING_ID_MORE_SETTINGS = 21;
 
+    /** Device setting ID for general bluetooth device header. */
+    int DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER = 22;
+
     /** Device setting ID for ANC. */
     int DEVICE_SETTING_ID_ANC = 1001;
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
index 38183d5..da01b3b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
@@ -32,9 +32,9 @@
  */
 data class DeviceSettingItem(
     @DeviceSettingId val settingId: Int,
-    val packageName: String,
-    val className: String,
-    val intentAction: String,
+    val packageName: String? = null,
+    val className: String? = null,
+    val intentAction: String? = null,
     val preferenceKey: String? = null,
     val highlighted: Boolean = false,
     val extras: Bundle = Bundle.EMPTY,
@@ -62,11 +62,11 @@
                     parcel.run {
                         DeviceSettingItem(
                             settingId = readInt(),
-                            packageName = readString() ?: "",
-                            className = readString() ?: "",
-                            intentAction = readString() ?: "",
+                            packageName = readString(),
+                            className = readString(),
+                            intentAction = readString(),
                             highlighted = readBoolean(),
-                            preferenceKey = readString() ?: "",
+                            preferenceKey = readString(),
                             extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
                         )
                     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
new file mode 100644
index 0000000..d837806
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSettingsConfigServiceStatus;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
new file mode 100644
index 0000000..ae86771
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * A data class representing a device settings config service status.
+ *
+ * @property success Whether the status is succeed.
+ * @property extras Extra bundle
+ */
+data class DeviceSettingsConfigServiceStatus(
+    val success: Boolean,
+    val extras: Bundle = Bundle.EMPTY,
+) : Parcelable {
+
+    override fun describeContents(): Int = 0
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.run {
+            writeBoolean(success)
+            writeBundle(extras)
+        }
+    }
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<DeviceSettingsConfigServiceStatus> =
+            object : Parcelable.Creator<DeviceSettingsConfigServiceStatus> {
+                override fun createFromParcel(parcel: Parcel) =
+                    parcel.run {
+                        DeviceSettingsConfigServiceStatus(
+                            success = readBoolean(),
+                            extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
+                        )
+                    }
+
+                override fun newArray(size: Int): Array<DeviceSettingsConfigServiceStatus?> {
+                    return arrayOfNulls(size)
+                }
+            }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
index 977849e..77d790e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
@@ -21,7 +21,7 @@
 import android.os.Parcelable
 
 /**
- * A data class representing a device settings item in bluetooth device details config.
+ * A data class representing a device settings provider service status.
  *
  * @property enabled Whether the service is enabled.
  * @property extras Extra bundle
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
index 647611e..9cf4907 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
@@ -17,8 +17,8 @@
 package com.android.settingslib.bluetooth.devicesettings;
 
 import com.android.settingslib.bluetooth.devicesettings.DeviceInfo;
-import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback;
 
 interface IDeviceSettingsConfigProviderService {
-   DeviceSettingsConfig getDeviceSettingsConfig(in DeviceInfo device);
+   oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback);
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
new file mode 100644
index 0000000..403cdd9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
@@ -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.settingslib.bluetooth.devicesettings;
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus;
+
+interface IGetDeviceSettingsConfigCallback {
+   oneway void onResult(in DeviceSettingsConfigServiceStatus status, in DeviceSettingsConfig config) = 0;
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 3d8ff86..4af0504 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -33,12 +33,15 @@
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
 import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +66,7 @@
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
+import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -74,22 +78,22 @@
     private val backgroundCoroutineContext: CoroutineContext,
 ) {
     data class EndPoint(
-        private val packageName: String,
+        private val packageName: String?,
         private val className: String?,
-        private val intentAction: String,
+        private val intentAction: String?,
     ) {
-        fun toIntent(): Intent =
-            Intent().apply {
+        fun toIntent(): Intent? {
+            if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(intentAction)) {
+                return null
+            }
+            return Intent().apply {
                 if (className.isNullOrBlank()) {
                     setPackage(packageName)
                 } else {
-                    setClassName(packageName, className)
+                    setClassName(packageName!!, className)
                 }
                 setAction(intentAction)
             }
-
-        fun isValid(): Boolean {
-            return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
         }
     }
 
@@ -126,8 +130,9 @@
                     when (it) {
                         is ServiceConnectionStatus.Connected ->
                             flowOf(
-                                it.service.getDeviceSettingsConfig(
-                                    deviceInfo { setBluetoothAddress(cachedDevice.address) }
+                                getDeviceSettingsConfigFromService(
+                                    deviceInfo { setBluetoothAddress(cachedDevice.address) },
+                                    it.service,
                                 )
                             )
                         ServiceConnectionStatus.Connecting -> flowOf()
@@ -137,6 +142,27 @@
                 .first()
         }
 
+    private suspend fun getDeviceSettingsConfigFromService(
+        deviceInfo: DeviceInfo,
+        service: IDeviceSettingsConfigProviderService,
+    ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation ->
+        service.getDeviceSettingsConfig(
+            deviceInfo,
+            object : IGetDeviceSettingsConfigCallback.Stub() {
+                override fun onResult(
+                    status: DeviceSettingsConfigServiceStatus,
+                    config: DeviceSettingsConfig?,
+                ) {
+                    if (!status.success) {
+                        continuation.resume(null)
+                    } else {
+                        continuation.resume(config)
+                    }
+                }
+            },
+        )
+    }
+
     private val settingIdToItemMapping =
         flow {
                 if (!isServiceEnabled.await()) {
@@ -160,6 +186,12 @@
             }
             .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1)
 
+    private val services =
+        ConcurrentHashMap<
+            EndPoint,
+            StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
+        >()
+
     /** Gets [DeviceSettingsConfig] for the device, return null when failed. */
     suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? {
         if (!isServiceEnabled.await()) {
@@ -222,24 +254,23 @@
                     )
                 }
             }
-            ?.filter { it.isValid() }
             ?.distinct()
-            ?.associateBy(
-                { it },
-                { endpoint ->
-                    services.computeIfAbsent(endpoint) {
-                        getService(
-                                endpoint.toIntent(),
-                                IDeviceSettingsProviderService.Stub::asInterface,
-                            )
-                            .stateIn(
-                                coroutineScope.plus(backgroundCoroutineContext),
-                                SharingStarted.WhileSubscribed(),
-                                ServiceConnectionStatus.Connecting,
-                            )
-                    }
-                },
-            )
+            ?.mapNotNull { endpoint ->
+                endpoint.toIntent()?.let { intent ->
+                    Pair(
+                        endpoint,
+                        services.computeIfAbsent(endpoint) {
+                            getService(intent, IDeviceSettingsProviderService.Stub::asInterface)
+                                .stateIn(
+                                    coroutineScope.plus(backgroundCoroutineContext),
+                                    SharingStarted.WhileSubscribed(),
+                                    ServiceConnectionStatus.Connecting,
+                                )
+                        },
+                    )
+                }
+            }
+            ?.toMap()
 
     private fun getDeviceSettingsFromService(
         cachedDevice: CachedBluetoothDevice,
@@ -320,11 +351,5 @@
         const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME"
         const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS"
         const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION"
-
-        val services =
-            ConcurrentHashMap<
-                EndPoint,
-                StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
-            >()
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index ef0f6cb..13a0601 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -42,6 +42,8 @@
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
+import java.util.Objects;
+
 /**
  * Drawable displaying a mobile cell signal indicator.
  */
@@ -90,6 +92,10 @@
     private int mCurrentDot;
 
     public SignalDrawable(Context context) {
+        this(context, new Handler());
+    }
+
+    public SignalDrawable(@NonNull Context context, @NonNull Handler handler) {
         super(context.getDrawable(ICON_RES));
         final String attributionPathString = context.getString(
                 com.android.internal.R.string.config_signalAttributionPath);
@@ -106,7 +112,7 @@
         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
-        mHandler = new Handler();
+        mHandler = handler;
         setDarkIntensity(0);
     }
 
@@ -304,6 +310,17 @@
                 | level;
     }
 
+    @Override
+    public boolean equals(@Nullable Object other) {
+        return other instanceof SignalDrawable
+                && ((SignalDrawable) other).getLevel() == this.getLevel();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getLevel());
+    }
+
     /** Returns the state representing empty mobile signal with the given number of levels. */
     public static int getEmptyState(int numLevels) {
         return getState(0, numLevels, true);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 0b8fb22ce..feaf7fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -97,7 +97,7 @@
             case TYPE_USB_ACCESSORY:
                 name =
                         inputRoutingEnabledAndIsDesktop()
-                                ? context.getString(R.string.media_transfer_usb_speaker_name)
+                                ? context.getString(R.string.media_transfer_usb_audio_name)
                                 : context.getString(R.string.media_transfer_wired_headphone_name);
                 break;
             case TYPE_DOCK:
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 712ddc8..5eeb49a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -167,6 +167,13 @@
         return this;
     }
 
+    public TestModeBuilder setVisualEffect(int effect, boolean allowed) {
+        ZenPolicy newPolicy = new ZenPolicy.Builder(mRule.getZenPolicy())
+                .showVisualEffect(effect, allowed).build();
+        setZenPolicy(newPolicy);
+        return this;
+    }
+
     public TestModeBuilder setEnabled(boolean enabled) {
         return setEnabled(enabled, /* byUser= */ false);
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
new file mode 100644
index 0000000..3149acf
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceSettingsConfigServiceStatusTest {
+
+    @Test
+    fun parcelOperation() {
+        val item =
+            DeviceSettingsConfigServiceStatus(
+                success = true,
+                extras = Bundle().apply { putString("key1", "value1") },
+            )
+
+        val fromParcel = writeAndRead(item)
+
+        assertThat(fromParcel.success).isEqualTo(item.success)
+        assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1"))
+    }
+
+    private fun writeAndRead(
+        item: DeviceSettingsConfigServiceStatus
+    ): DeviceSettingsConfigServiceStatus {
+        val parcel = Parcel.obtain()
+        item.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+        return DeviceSettingsConfigServiceStatus.CREATOR.createFromParcel(parcel)
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 0cb6bc1..4e62fd3b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -33,10 +33,12 @@
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState
 import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
@@ -53,7 +55,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -140,15 +141,10 @@
             )
     }
 
-    @After
-    fun clean() {
-        DeviceSettingServiceConnection.services.clear()
-    }
-
     @Test
     fun getDeviceSettingsConfig_withMetadata_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(true))
             `when`(settingProviderService2.serviceStatus)
@@ -179,7 +175,7 @@
                     )
                 )
                 .thenReturn("".toByteArray())
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(true))
             `when`(settingProviderService2.serviceStatus)
@@ -194,7 +190,7 @@
     @Test
     fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(false))
             `when`(settingProviderService2.serviceStatus)
@@ -209,7 +205,7 @@
     @Test
     fun getDeviceSettingsConfig_bindingServiceFail_returnNull() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any())
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -221,7 +217,7 @@
     @Test
     fun getDeviceSetting_actionSwitchPreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -247,7 +243,7 @@
     @Test
     fun getDeviceSetting_multiTogglePreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -273,7 +269,7 @@
     @Test
     fun getDeviceSetting_helpPreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -299,6 +295,7 @@
     @Test
     fun getDeviceSetting_noConfig_returnNull() {
         testScope.runTest {
+            setUpConfigService(false, null)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -320,7 +317,7 @@
     @Test
     fun updateDeviceSettingState_switchState_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -358,7 +355,7 @@
     @Test
     fun updateDeviceSettingState_multiToggleState_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -459,6 +456,17 @@
         assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
     }
 
+    private fun setUpConfigService(success: Boolean, config: DeviceSettingsConfig?) {
+        `when`(configService.getDeviceSettingsConfig(any(), any())).then { input ->
+            input
+                .getArgument<IGetDeviceSettingsConfigCallback>(1)
+                .onResult(
+                    DeviceSettingsConfigServiceStatus(success = success),
+                    config
+                )
+        }
+    }
+
     private companion object {
         const val BLUETOOTH_ADDRESS = "12:34:56:78"
         const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice"
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 da5f428..1739c0e5 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
@@ -136,7 +136,7 @@
         when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE);
 
         assertThat(mPhoneMediaDevice.getName())
-                .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name));
+                .isEqualTo(mContext.getString(R.string.media_transfer_usb_audio_name));
 
         when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d710939..5e31da4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -164,6 +164,7 @@
         Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
         Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
         Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+        Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
         Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
         Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
         Settings.Secure.ZEN_DURATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index fa16a44..b3f7374 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -243,6 +243,7 @@
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index f8383d9..a9d4c89 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -40,16 +40,6 @@
 }
 
 flag {
-   name: "notification_minimalism_prototype"
-   namespace: "systemui"
-   description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
-   bug: "330387368"
-   metadata {
-        purpose: PURPOSE_BUGFIX
-   }
-}
-
-flag {
    name: "notification_view_flipper_pausing_v2"
    namespace: "systemui"
    description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
@@ -1128,6 +1118,16 @@
 }
 
 flag {
+  name: "media_controls_umo_inflation_in_background"
+  namespace: "systemui"
+  description: "Inflate UMO in background thread"
+  bug: "368514198"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   namespace: "systemui"
   name: "enable_view_capture_tracing"
   description: "Enables view capture tracing in System UI."
@@ -1431,4 +1431,4 @@
    metadata {
        purpose: PURPOSE_BUGFIX
    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/animation/lib/Android.bp b/packages/SystemUI/animation/lib/Android.bp
index 4324d463..d9230ec 100644
--- a/packages/SystemUI/animation/lib/Android.bp
+++ b/packages/SystemUI/animation/lib/Android.bp
@@ -33,6 +33,20 @@
     ],
 }
 
+// This is the core animation library written in java and can be depended by java sdk libraries.
+// Please don't introduce kotlin code in this target since kotlin is incompatible with sdk
+// libraries.
+java_library {
+    name: "PlatformAnimationLib-core",
+    srcs: [
+        "src/com/android/systemui/animation/*.java",
+        ":PlatformAnimationLib-aidl",
+    ],
+    static_libs: [
+        "WindowManager-Shell-shared",
+    ],
+}
+
 filegroup {
     name: "PlatformAnimationLib-aidl",
     srcs: [
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
new file mode 100644
index 0000000..08db95e
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -0,0 +1,374 @@
+/*
+ * 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.animation;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
+import android.window.WindowAnimationState;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.shared.TransitionUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
+ * and automatically attaches it to the transition leash before the transition starts.
+ */
+public class OriginRemoteTransition extends IRemoteTransition.Stub {
+    private static final String TAG = "OriginRemoteTransition";
+
+    private final Context mContext;
+    private final boolean mIsEntry;
+    private final UIComponent mOrigin;
+    private final TransitionPlayer mPlayer;
+    private final long mDuration;
+    private final Handler mHandler;
+
+    @Nullable private SurfaceControl.Transaction mStartTransaction;
+    @Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
+    @Nullable private UIComponent.Transaction mOriginTransaction;
+    @Nullable private ValueAnimator mAnimator;
+    @Nullable private SurfaceControl mOriginLeash;
+    private boolean mCancelled;
+
+    OriginRemoteTransition(
+            Context context,
+            boolean isEntry,
+            UIComponent origin,
+            TransitionPlayer player,
+            long duration,
+            Handler handler) {
+        mContext = context;
+        mIsEntry = isEntry;
+        mOrigin = origin;
+        mPlayer = player;
+        mDuration = duration;
+        mHandler = handler;
+    }
+
+    @Override
+    public void startAnimation(
+            IBinder token,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("startAnimation - " + info);
+        mHandler.post(
+                () -> {
+                    mStartTransaction = t;
+                    mFinishCallback = finishCallback;
+                    startAnimationInternal(info);
+                });
+    }
+
+    @Override
+    public void mergeAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IBinder mergeTarget,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("mergeAnimation - " + info);
+        mHandler.post(this::cancel);
+    }
+
+    @Override
+    public void takeOverAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback,
+            WindowAnimationState[] states) {
+        logD("takeOverAnimation - " + info);
+    }
+
+    @Override
+    public void onTransitionConsumed(IBinder transition, boolean aborted) {
+        logD("onTransitionConsumed - aborted: " + aborted);
+        mHandler.post(this::cancel);
+    }
+
+    private void startAnimationInternal(TransitionInfo info) {
+        if (!prepareUIs(info)) {
+            logE("Unable to prepare UI!");
+            finishAnimation(/* finished= */ false);
+            return;
+        }
+        // Notify player that we are starting.
+        mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction);
+
+        // Start the animator.
+        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mAnimator.setDuration(mDuration);
+        mAnimator.addListener(
+                new AnimatorListener() {
+                    @Override
+                    public void onAnimationStart(Animator a) {}
+
+                    @Override
+                    public void onAnimationEnd(Animator a) {
+                        finishAnimation(/* finished= */ !mCancelled);
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator a) {
+                        mCancelled = true;
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animator a) {}
+                });
+        mAnimator.addUpdateListener(
+                a -> {
+                    mPlayer.onProgress((float) a.getAnimatedValue());
+                });
+        mAnimator.start();
+    }
+
+    private boolean prepareUIs(TransitionInfo info) {
+        if (info.getRootCount() == 0) {
+            logE("prepareUIs: no root leash!");
+            return false;
+        }
+        if (info.getRootCount() > 1) {
+            logE("prepareUIs: multi-display transition is not supported yet!");
+            return false;
+        }
+        if (info.getChanges().isEmpty()) {
+            logE("prepareUIs: no changes!");
+            return false;
+        }
+
+        SurfaceControl rootLeash = info.getRoot(0).getLeash();
+        int displayId = info.getChanges().get(0).getEndDisplayId();
+        Rect displayBounds = getDisplayBounds(displayId);
+        float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);
+
+        // Create the origin leash and add to the transition root leash.
+        mOriginLeash =
+                new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
+        mStartTransaction
+                .reparent(mOriginLeash, rootLeash)
+                .show(mOriginLeash)
+                .setCornerRadius(mOriginLeash, windowRadius)
+                .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());
+
+        // Process surfaces
+        List<SurfaceControl> openingSurfaces = new ArrayList<>();
+        List<SurfaceControl> closingSurfaces = new ArrayList<>();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            SurfaceControl leash = change.getLeash();
+            // Reparent leash to the transition root.
+            mStartTransaction.reparent(leash, rootLeash);
+            if (TransitionUtil.isOpeningMode(mode)) {
+                openingSurfaces.add(change.getLeash());
+                // For opening surfaces, ending bounds are base bound. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getEndAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            } else if (TransitionUtil.isClosingMode(mode)) {
+                closingSurfaces.add(change.getLeash());
+                // For closing surfaces, starting bounds are base bounds. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getStartAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            }
+        }
+
+        // Set relative order:
+        // ----  App1  ----
+        // ---- origin ----
+        // ----  App2  ----
+        if (mIsEntry) {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
+        } else {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
+        }
+
+        // Attach origin UIComponent to origin leash.
+        mOriginTransaction = mOrigin.newTransaction();
+        mOriginTransaction
+                .attachToTransitionLeash(
+                        mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height())
+                .commit();
+
+        // Apply all surface changes.
+        mStartTransaction.apply();
+        return true;
+    }
+
+    private Rect getDisplayBounds(int displayId) {
+        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+        DisplayMetrics metrics = new DisplayMetrics();
+        dm.getDisplay(displayId).getMetrics(metrics);
+        return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
+    }
+
+    private void finishAnimation(boolean finished) {
+        logD("finishAnimation: finished=" + finished);
+        if (mAnimator == null) {
+            // The transition didn't start. Ensure we apply the start transaction and report
+            // finish afterwards.
+            mStartTransaction
+                    .addTransactionCommittedListener(
+                            mContext.getMainExecutor(), this::finishInternal)
+                    .apply();
+            return;
+        }
+        mAnimator = null;
+        // Notify client that we have ended.
+        mPlayer.onEnd(finished);
+        // Detach the origin from the transition leash and report finish after it's done.
+        mOriginTransaction
+                .detachFromTransitionLeash(
+                        mOrigin, mContext.getMainExecutor(), this::finishInternal)
+                .commit();
+    }
+
+    private void finishInternal() {
+        logD("finishInternal");
+        if (mOriginLeash != null) {
+            // Release origin leash.
+            mOriginLeash.release();
+            mOriginLeash = null;
+        }
+        try {
+            mFinishCallback.onTransitionFinished(null, null);
+        } catch (RemoteException e) {
+            logE("Unable to report transition finish!", e);
+        }
+        mStartTransaction = null;
+        mOriginTransaction = null;
+        mFinishCallback = null;
+    }
+
+    private void cancel() {
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+    }
+
+    private static void logD(String msg) {
+        if (OriginTransitionSession.DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private static void logE(String msg) {
+        Log.e(TAG, msg);
+    }
+
+    private static void logE(String msg, Throwable e) {
+        Log.e(TAG, msg, e);
+    }
+
+    private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
+        List<SurfaceControl> surfaces = new ArrayList<>();
+        Rect maxBounds = new Rect();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            if (TransitionUtil.isOpeningMode(mode) == isOpening) {
+                surfaces.add(change.getLeash());
+                Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
+                maxBounds.union(bounds);
+            }
+        }
+        return new SurfaceUIComponent(
+                surfaces,
+                /* alpha= */ 1.0f,
+                /* visible= */ true,
+                /* bounds= */ maxBounds,
+                /* baseBounds= */ maxBounds);
+    }
+
+    /** An interface that represents an origin transitions. */
+    public interface TransitionPlayer {
+
+        /**
+         * Called when an origin transition starts. This method exposes the raw {@link
+         * TransitionInfo} so that clients can extract more information from it.
+         */
+        default void onStart(
+                TransitionInfo transitionInfo,
+                SurfaceControl.Transaction sfTransaction,
+                UIComponent origin,
+                UIComponent.Transaction uiTransaction) {
+            // Wrap transactions.
+            Transactions transactions =
+                    new Transactions()
+                            .registerTransactionForClass(origin.getClass(), uiTransaction)
+                            .registerTransactionForClass(
+                                    SurfaceUIComponent.class,
+                                    new SurfaceUIComponent.Transaction(sfTransaction));
+            // Wrap surfaces and start.
+            onStart(
+                    transactions,
+                    origin,
+                    wrapSurfaces(transitionInfo, /* isOpening= */ false),
+                    wrapSurfaces(transitionInfo, /* isOpening= */ true));
+        }
+
+        /**
+         * Called when an origin transition starts. This method exposes the opening and closing
+         * windows as wrapped {@link UIComponent} to provide simplified interface to clients.
+         */
+        void onStart(
+                UIComponent.Transaction transaction,
+                UIComponent origin,
+                UIComponent closingApp,
+                UIComponent openingApp);
+
+        /** Called to update the transition frame. */
+        void onProgress(float progress);
+
+        /** Called when the transition ended. */
+        void onEnd(boolean finished);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
new file mode 100644
index 0000000..23693b6
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -0,0 +1,321 @@
+/*
+ * 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.animation;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ActivityOptions.LaunchCookie;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
+
+import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer;
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A session object that holds origin transition states for starting an activity from an on-screen
+ * UI component and smoothly transitioning back from the activity to the same UI component.
+ */
+public class OriginTransitionSession {
+    private static final String TAG = "OriginTransitionSession";
+    static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {NOT_STARTED, STARTED, CANCELLED})
+    private @interface State {}
+
+    @State private static final int NOT_STARTED = 0;
+    @State private static final int STARTED = 1;
+    @State private static final int CANCELLED = 5;
+
+    private final String mName;
+    @Nullable private final IOriginTransitions mOriginTransitions;
+    private final Predicate<RemoteTransition> mIntentStarter;
+    @Nullable private final IRemoteTransition mEntryTransition;
+    @Nullable private final IRemoteTransition mExitTransition;
+    private final AtomicInteger mState = new AtomicInteger(NOT_STARTED);
+
+    @Nullable private RemoteTransition mOriginTransition;
+
+    private OriginTransitionSession(
+            String name,
+            @Nullable IOriginTransitions originTransitions,
+            Predicate<RemoteTransition> intentStarter,
+            @Nullable IRemoteTransition entryTransition,
+            @Nullable IRemoteTransition exitTransition) {
+        mName = name;
+        mOriginTransitions = originTransitions;
+        mIntentStarter = intentStarter;
+        mEntryTransition = entryTransition;
+        mExitTransition = exitTransition;
+        if (hasExitTransition() && !hasEntryTransition()) {
+            throw new IllegalArgumentException(
+                    "Entry transition must be supplied if you want to play an exit transition!");
+        }
+    }
+
+    /**
+     * Launch the target intent with the supplied entry transition. After this method, the entry
+     * transition is expected to receive callbacks. The exit transition will be registered and
+     * triggered when the system detects a return from the launched activity to the launching
+     * activity.
+     */
+    public boolean start() {
+        if (!mState.compareAndSet(NOT_STARTED, STARTED)) {
+            logE("start: illegal state - " + stateToString(mState.get()));
+            return false;
+        }
+
+        RemoteTransition remoteTransition = null;
+        if (hasEntryTransition() && hasExitTransition()) {
+            logD("start: starting with entry and exit transition.");
+            try {
+                remoteTransition =
+                        mOriginTransition =
+                                mOriginTransitions.makeOriginTransition(
+                                        new RemoteTransition(mEntryTransition, mName + "-entry"),
+                                        new RemoteTransition(mExitTransition, mName + "-exit"));
+            } catch (RemoteException e) {
+                logE("Unable to create origin transition!", e);
+            }
+        } else if (hasEntryTransition()) {
+            logD("start: starting with entry transition.");
+            remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry");
+
+        } else {
+            logD("start: starting without transition.");
+        }
+        if (mIntentStarter.test(remoteTransition)) {
+            return true;
+        } else {
+            // Animation is cancelled by intent starter.
+            logD("start: cancelled by intent starter!");
+            cancel();
+            return false;
+        }
+    }
+
+    /**
+     * Cancel the current transition and the registered exit transition if it exists. After this
+     * method, this session object can no longer be used. Clients need to create a new session
+     * object if they want to launch another intent with origin transition.
+     */
+    public void cancel() {
+        final int lastState = mState.getAndSet(CANCELLED);
+        if (lastState == CANCELLED || lastState == NOT_STARTED) {
+            return;
+        }
+        logD("cancel: cancelled transition. Last state: " + stateToString(lastState));
+        if (mOriginTransition != null) {
+            try {
+                mOriginTransitions.cancelOriginTransition(mOriginTransition);
+                mOriginTransition = null;
+            } catch (RemoteException e) {
+                logE("Unable to cancel origin transition!", e);
+            }
+        }
+    }
+
+    private boolean hasEntryTransition() {
+        return mEntryTransition != null;
+    }
+
+    private boolean hasExitTransition() {
+        return mOriginTransitions != null && mExitTransition != null;
+    }
+
+    private void logD(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, "Session[" + mName + "] - " + msg);
+        }
+    }
+
+    private void logE(String msg) {
+        Log.e(TAG, "Session[" + mName + "] - " + msg);
+    }
+
+    private void logE(String msg, Throwable e) {
+        Log.e(TAG, "Session[" + mName + "] - " + msg, e);
+    }
+
+    private static String stateToString(@State int state) {
+        switch (state) {
+            case NOT_STARTED:
+                return "NOT_STARTED";
+            case STARTED:
+                return "STARTED";
+            case CANCELLED:
+                return "CANCELLED";
+            default:
+                return "UNKNOWN(" + state + ")";
+        }
+    }
+
+    /** A builder to build a {@link OriginTransitionSession}. */
+    public static class Builder {
+        private final Context mContext;
+        @Nullable private final IOriginTransitions mOriginTransitions;
+        @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
+        @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+        private Handler mHandler = new Handler(Looper.getMainLooper());
+        private String mName;
+        @Nullable private Predicate<RemoteTransition> mIntentStarter;
+
+        /** Create a builder that only supports entry transition. */
+        public Builder(Context context) {
+            this(context, /* originTransitions= */ null);
+        }
+
+        /** Create a builder that supports both entry and return transition. */
+        public Builder(Context context, @Nullable IOriginTransitions originTransitions) {
+            mContext = context;
+            mOriginTransitions = originTransitions;
+            mName = context.getPackageName();
+        }
+
+        /** Specify a name that is used in logging. */
+        public Builder withName(String name) {
+            mName = name;
+            return this;
+        }
+
+        /** Specify an intent that will be launched when the session started. */
+        public Builder withIntent(Intent intent) {
+            return withIntentStarter(
+                    transition -> {
+                        mContext.startActivity(
+                                intent, createDefaultActivityOptions(transition).toBundle());
+                        return true;
+                    });
+        }
+
+        /** Specify a pending intent that will be launched when the session started. */
+        public Builder withPendingIntent(PendingIntent pendingIntent) {
+            return withIntentStarter(
+                    transition -> {
+                        try {
+                            pendingIntent.send(createDefaultActivityOptions(transition).toBundle());
+                            return true;
+                        } catch (PendingIntent.CanceledException e) {
+                            Log.e(TAG, "Failed to launch pending intent!", e);
+                            return false;
+                        }
+                    });
+        }
+
+        private static ActivityOptions createDefaultActivityOptions(
+                @Nullable RemoteTransition transition) {
+            ActivityOptions options =
+                    transition == null
+                            ? ActivityOptions.makeBasic()
+                            : ActivityOptions.makeRemoteTransition(transition);
+            LaunchCookie cookie = new LaunchCookie();
+            options.setLaunchCookie(cookie);
+            return options;
+        }
+
+        /**
+         * Specify an intent starter function that will be called to start an activity. The function
+         * accepts an optional {@link RemoteTransition} object which can be used to create an {@link
+         * ActivityOptions} for the activity launch. The function can also return a {@code false}
+         * result to cancel the session.
+         *
+         * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link
+         * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default
+         * activity launch code doesn't satisfy your requirement.
+         */
+        public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) {
+            mIntentStarter = intentStarter;
+            return this;
+        }
+
+        /** Add an entry transition to the builder. */
+        public Builder withEntryTransition(IRemoteTransition transition) {
+            mEntryTransitionSupplier = () -> transition;
+            return this;
+        }
+
+        /** Add an origin entry transition to the builder. */
+        public Builder withEntryTransition(
+                UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) {
+            mEntryTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ true,
+                                    entryOrigin,
+                                    entryPlayer,
+                                    entryDuration,
+                                    mHandler);
+            return this;
+        }
+
+        /** Add an exit transition to the builder. */
+        public Builder withExitTransition(IRemoteTransition transition) {
+            mExitTransitionSupplier = () -> transition;
+            return this;
+        }
+
+        /** Add an origin exit transition to the builder. */
+        public Builder withExitTransition(
+                UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) {
+            mExitTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ false,
+                                    exitTarget,
+                                    exitPlayer,
+                                    exitDuration,
+                                    mHandler);
+            return this;
+        }
+
+        /** Supply a handler where transition callbacks will run. */
+        public Builder withHandler(Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
+        /** Build an {@link OriginTransitionSession}. */
+        public OriginTransitionSession build() {
+            if (mIntentStarter == null) {
+                throw new IllegalArgumentException("No intent, pending intent, or intent starter!");
+            }
+            return new OriginTransitionSession(
+                    mName,
+                    mOriginTransitions,
+                    mIntentStarter,
+                    mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(),
+                    mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get());
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
new file mode 100644
index 0000000..2438736
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
@@ -0,0 +1,169 @@
+/*
+ * 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.animation;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+/** A {@link UIComponent} representing a {@link SurfaceControl}. */
+public class SurfaceUIComponent implements UIComponent {
+    private final Collection<SurfaceControl> mSurfaces;
+    private final Rect mBaseBounds;
+    private final float[] mFloat9 = new float[9];
+
+    private float mAlpha;
+    private boolean mVisible;
+    private Rect mBounds;
+
+    public SurfaceUIComponent(
+            SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) {
+        this(Arrays.asList(sc), alpha, visible, bounds, baseBounds);
+    }
+
+    public SurfaceUIComponent(
+            Collection<SurfaceControl> surfaces,
+            float alpha,
+            boolean visible,
+            Rect bounds,
+            Rect baseBounds) {
+        mSurfaces = surfaces;
+        mAlpha = alpha;
+        mVisible = visible;
+        mBounds = bounds;
+        mBaseBounds = baseBounds;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mVisible;
+    }
+
+    @Override
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction(new SurfaceControl.Transaction());
+    }
+
+    @Override
+    public String toString() {
+        return "SurfaceUIComponent{mSurfaces="
+                + mSurfaces
+                + ", mAlpha="
+                + mAlpha
+                + ", mVisible="
+                + mVisible
+                + ", mBounds="
+                + mBounds
+                + ", mBaseBounds="
+                + mBaseBounds
+                + "}";
+    }
+
+    /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */
+    public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> {
+        private final SurfaceControl.Transaction mTransaction;
+        private final ArrayList<Runnable> mChanges = new ArrayList<>();
+
+        public Transaction(SurfaceControl.Transaction transaction) {
+            mTransaction = transaction;
+        }
+
+        @Override
+        public Transaction setAlpha(SurfaceUIComponent ui, float alpha) {
+            mChanges.add(
+                    () -> {
+                        ui.mAlpha = alpha;
+                        ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(SurfaceUIComponent ui, boolean visible) {
+            mChanges.add(
+                    () -> {
+                        ui.mVisible = visible;
+                        if (visible) {
+                            ui.mSurfaces.forEach(s -> mTransaction.show(s));
+                        } else {
+                            ui.mSurfaces.forEach(s -> mTransaction.hide(s));
+                        }
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) {
+            mChanges.add(
+                    () -> {
+                        if (ui.mBounds.equals(bounds)) {
+                            return;
+                        }
+                        ui.mBounds = bounds;
+                        Matrix matrix = new Matrix();
+                        matrix.setRectToRect(
+                                new RectF(ui.mBaseBounds),
+                                new RectF(ui.mBounds),
+                                Matrix.ScaleToFit.FILL);
+                        ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(
+                    () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash)));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                SurfaceUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(
+                    () -> {
+                        ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null));
+                        mTransaction.addTransactionCommittedListener(executor, onDone::run);
+                    });
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+            mTransaction.apply();
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
new file mode 100644
index 0000000..5240d99
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
@@ -0,0 +1,86 @@
+/*
+ * 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.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * A composite {@link UIComponent.Transaction} that combines multiple other transactions for each ui
+ * type.
+ */
+public class Transactions implements UIComponent.Transaction<UIComponent> {
+    private final Map<Class, UIComponent.Transaction> mTransactions = new ArrayMap<>();
+
+    /** Register a transaction object for updating a certain {@link UIComponent} type. */
+    public <T extends UIComponent> Transactions registerTransactionForClass(
+            Class<T> clazz, UIComponent.Transaction transaction) {
+        mTransactions.put(clazz, transaction);
+        return this;
+    }
+
+    private UIComponent.Transaction getTransactionFor(UIComponent ui) {
+        UIComponent.Transaction transaction = mTransactions.get(ui.getClass());
+        if (transaction == null) {
+            transaction = ui.newTransaction();
+            mTransactions.put(ui.getClass(), transaction);
+        }
+        return transaction;
+    }
+
+    @Override
+    public Transactions setAlpha(UIComponent ui, @FloatRange(from = 0.0, to = 1.0) float alpha) {
+        getTransactionFor(ui).setAlpha(ui, alpha);
+        return this;
+    }
+
+    @Override
+    public Transactions setVisible(UIComponent ui, boolean visible) {
+        getTransactionFor(ui).setVisible(ui, visible);
+        return this;
+    }
+
+    @Override
+    public Transactions setBounds(UIComponent ui, Rect bounds) {
+        getTransactionFor(ui).setBounds(ui, bounds);
+        return this;
+    }
+
+    @Override
+    public Transactions attachToTransitionLeash(
+            UIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+        getTransactionFor(ui).attachToTransitionLeash(ui, transitionLeash, w, h);
+        return this;
+    }
+
+    @Override
+    public Transactions detachFromTransitionLeash(
+            UIComponent ui, Executor executor, Runnable onDone) {
+        getTransactionFor(ui).detachFromTransitionLeash(ui, executor, onDone);
+        return this;
+    }
+
+    @Override
+    public void commit() {
+        mTransactions.values().forEach(UIComponent.Transaction::commit);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
new file mode 100644
index 0000000..747e4d1
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import java.util.concurrent.Executor;
+
+/** An interface representing an UI component on the display. */
+public interface UIComponent {
+
+    /** Get the current alpha of this UI. */
+    float getAlpha();
+
+    /** Check if this UI is visible. */
+    boolean isVisible();
+
+    /** Get the bounds of this UI in its display. */
+    Rect getBounds();
+
+    /** Create a new {@link Transaction} that can update this UI. */
+    Transaction newTransaction();
+
+    /**
+     * A transaction class for updating {@link UIComponent}.
+     *
+     * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle.
+     */
+    interface Transaction<T extends UIComponent> {
+        /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha);
+
+        /**
+         * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called.
+         */
+        Transaction setVisible(T ui, boolean visible);
+
+        /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setBounds(T ui, Rect bounds);
+
+        /**
+         * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is
+         * called.
+         */
+        Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h);
+
+        /**
+         * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is
+         * called.
+         */
+        Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone);
+
+        /** Commit any pending changes added to this transaction. */
+        void commit();
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
new file mode 100644
index 0000000..313789c
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -0,0 +1,278 @@
+/*
+ * 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.animation;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
+ * class will draw the content of the {@link View} directly into the leash, and the actual View will
+ * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
+ * full-screen size leash without being constrained by the view tree's boundary or inheriting its
+ * parent's alpha and transformation.
+ */
+public class ViewUIComponent implements UIComponent {
+    private static final String TAG = "ViewUIComponent";
+    private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+    private final OnDrawListener mOnDrawListener = this::postDraw;
+    private final View mView;
+
+    @Nullable private SurfaceControl mSurfaceControl;
+    @Nullable private Surface mSurface;
+    @Nullable private Rect mViewBoundsOverride;
+    private boolean mVisibleOverride;
+    private boolean mDirty;
+
+    public ViewUIComponent(View view) {
+        mView = view;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mView.getAlpha();
+    }
+
+    @Override
+    public boolean isVisible() {
+        return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
+    }
+
+    @Override
+    public Rect getBounds() {
+        if (isAttachedToLeash() && mViewBoundsOverride != null) {
+            return mViewBoundsOverride;
+        }
+        return getRealBounds();
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction();
+    }
+
+    private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
+        logD("attachToTransitionLeash");
+        // Remember current visibility.
+        mVisibleOverride = mView.getVisibility() == View.VISIBLE;
+
+        // Create the surface
+        mSurfaceControl =
+                new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
+        mSurface = new Surface(mSurfaceControl);
+        forceDraw();
+
+        // Attach surface to transition leash
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
+
+        // Make sure view draw triggers surface draw.
+        mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+
+        // Make the view invisible AFTER the surface is shown.
+        t.addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> mView.setVisibility(View.INVISIBLE))
+                .apply();
+    }
+
+    private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
+        logD("detachFromTransitionLeash");
+        Surface s = mSurface;
+        SurfaceControl sc = mSurfaceControl;
+        mSurface = null;
+        mSurfaceControl = null;
+        mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+        // Restore view visibility
+        mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
+        mView.invalidate();
+        // Clean up surfaces.
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(sc, null)
+                .addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> {
+                            s.release();
+                            sc.release();
+                            executor.execute(onDone);
+                        });
+        // Apply transaction AFTER the view is drawn.
+        mView.getRootSurfaceControl().applyTransactionOnDraw(t);
+    }
+
+    @Override
+    public String toString() {
+        return "ViewUIComponent{"
+                + "alpha="
+                + getAlpha()
+                + ", visible="
+                + isVisible()
+                + ", bounds="
+                + getBounds()
+                + ", attached="
+                + isAttachedToLeash()
+                + "}";
+    }
+
+    private void draw() {
+        if (!mDirty) {
+            // No need to draw. This is probably a duplicate call.
+            logD("draw: skipped - clean");
+            return;
+        }
+        mDirty = false;
+        if (!isAttachedToLeash()) {
+            // Not attached.
+            logD("draw: skipped - not attached");
+            return;
+        }
+        ViewGroup.LayoutParams params = mView.getLayoutParams();
+        if (params == null || params.width == 0 || params.height == 0) {
+            // layout pass didn't happen.
+            logD("draw: skipped - no layout");
+            return;
+        }
+        Canvas canvas = mSurface.lockHardwareCanvas();
+        // Clear the canvas first.
+        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        if (mVisibleOverride) {
+            Rect realBounds = getRealBounds();
+            Rect renderBounds = getBounds();
+            canvas.translate(renderBounds.left, renderBounds.top);
+            canvas.scale(
+                    (float) renderBounds.width() / realBounds.width(),
+                    (float) renderBounds.height() / realBounds.height());
+            canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
+            mView.draw(canvas);
+            canvas.restore();
+        }
+        mSurface.unlockCanvasAndPost(canvas);
+        logD("draw: done");
+    }
+
+    private void forceDraw() {
+        mDirty = true;
+        draw();
+    }
+
+    private Rect getRealBounds() {
+        Rect output = new Rect();
+        mView.getBoundsOnScreen(output);
+        return output;
+    }
+
+    private boolean isAttachedToLeash() {
+        return mSurfaceControl != null && mSurface != null;
+    }
+
+    private void logD(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private void setVisible(boolean visible) {
+        logD("setVisibility: " + visible);
+        if (isAttachedToLeash()) {
+            mVisibleOverride = visible;
+            postDraw();
+        } else {
+            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    private void setBounds(Rect bounds) {
+        logD("setBounds: " + bounds);
+        mViewBoundsOverride = bounds;
+        if (isAttachedToLeash()) {
+            postDraw();
+        } else {
+            Log.w(TAG, "setBounds: not attached to leash!");
+        }
+    }
+
+    private void setAlpha(float alpha) {
+        logD("setAlpha: " + alpha);
+        mView.setAlpha(alpha);
+        if (isAttachedToLeash()) {
+            postDraw();
+        }
+    }
+
+    private void postDraw() {
+        if (mDirty) {
+            return;
+        }
+        mDirty = true;
+        mView.post(this::draw);
+    }
+
+    public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
+        private final List<Runnable> mChanges = new ArrayList<>();
+
+        @Override
+        public Transaction setAlpha(ViewUIComponent ui, float alpha) {
+            mChanges.add(() -> ui.setAlpha(alpha));
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(ViewUIComponent ui, boolean visible) {
+            mChanges.add(() -> ui.setVisible(visible));
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
+            mChanges.add(() -> ui.setBounds(bounds));
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(() -> ui.attachToTransitionLeash(transitionLeash, w, h));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                ViewUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(() -> ui.detachFromTransitionLeash(executor, onDone));
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/tests/Android.bp b/packages/SystemUI/animation/lib/tests/Android.bp
new file mode 100644
index 0000000..c1a3e84
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+    name: "PlatformAnimationLibCoreTests",
+
+    defaults: [
+        "platform_app_defaults",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    dxflags: ["--multi-dex"],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    static_libs: [
+        "PlatformAnimationLib-core",
+        "platform-test-rules",
+        "testables",
+    ],
+    compile_multilib: "both",
+    libs: [
+        "android.test.runner.stubs.system",
+        "android.test.base.stubs.system",
+    ],
+
+    certificate: "platform",
+
+    manifest: "AndroidManifest.xml",
+}
diff --git a/packages/SystemUI/animation/lib/tests/AndroidManifest.xml b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..1788abf
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:sharedUserId="android.uid.system"
+    package="com.android.systemui.animation.core.tests" >
+
+     <application android:debuggable="true" android:testOnly="true">
+         <uses-library android:name="android.test.runner" />
+     </application>
+
+    <instrumentation android:name="android.testing.TestableInstrumentation"
+        android:targetPackage="com.android.systemui.animation.core.tests"
+        android:label="Tests for PlatformAnimationLib-core" />
+</manifest>
diff --git a/packages/SystemUI/animation/lib/tests/AndroidTest.xml b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
new file mode 100644
index 0000000..0f37d7a
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Runs Tests for PlatformAnimationLib-core.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PlatformAnimationLibCoreTests.apk" />
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+
+    <!-- Among other reasons, root access is needed for screen recording artifacts. -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+        <option name="force-root" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.systemui.animation.core.tests" />
+        <option name="runner" value="android.testing.TestableInstrumentation" />
+        <option name="test-filter-dir" value="/data/data/com.android.systemui.animation.core.tests" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/user/0/com.android.systemui.animation.core.tests/files" />
+        <option name="collect-on-run-ended-only" value="false" />
+    </metrics_collector>
+</configuration>
diff --git a/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
new file mode 100644
index 0000000..287e53b
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+/** Unit tests for {@link OriginTransitionSession}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public final class OriginTransitionSessionTest {
+    private static final ComponentName TEST_ACTIVITY_1 = new ComponentName("test", "Activity1");
+    private static final ComponentName TEST_ACTIVITY_2 = new ComponentName("test", "Activity2");
+    private static final ComponentName TEST_ACTIVITY_3 = new ComponentName("test", "Activity3");
+
+    private FakeIOriginTransitions mIOriginTransitions;
+    private Instrumentation mInstrumentation;
+    private FakeIntentStarter mIntentStarter;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+        mIOriginTransitions = new FakeIOriginTransitions();
+        mIntentStarter = new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2);
+    }
+
+    @Test
+    public void sessionStart_withEntryAndExitTransition_transitionsPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(entry.started()).isTrue();
+
+        runReturnTransition(mIntentStarter);
+
+        assertThat(exit.started()).isTrue();
+    }
+
+    @Test
+    public void sessionStart_withEntryTransition_transitionPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(entry.started()).isTrue();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_withoutTransition_launchedIntent() {
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_cancelledByIntentStarter_transitionNotPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        mIntentStarter =
+                new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2, /* result= */ false);
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+        assertThat(exit.started()).isFalse();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_alreadyStarted_noOp() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+        session.start();
+        entry.reset();
+        mIntentStarter.reset();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_alreadyCancelled_noOp() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+        session.cancel();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionCancelled_returnTransitionNotPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+        session.cancel();
+
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void multipleSessionsStarted_allTransitionsPlayed() {
+        FakeRemoteTransition entry1 = new FakeRemoteTransition();
+        FakeRemoteTransition exit1 = new FakeRemoteTransition();
+        FakeIntentStarter starter1 = mIntentStarter;
+        OriginTransitionSession session1 =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(starter1)
+                        .withEntryTransition(entry1)
+                        .withExitTransition(exit1)
+                        .build();
+        FakeRemoteTransition entry2 = new FakeRemoteTransition();
+        FakeRemoteTransition exit2 = new FakeRemoteTransition();
+        FakeIntentStarter starter2 = new FakeIntentStarter(TEST_ACTIVITY_2, TEST_ACTIVITY_3);
+        OriginTransitionSession session2 =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(starter2)
+                        .withEntryTransition(entry2)
+                        .withExitTransition(exit2)
+                        .build();
+
+        session1.start();
+
+        assertThat(starter1.hasLaunched()).isTrue();
+        assertThat(entry1.started()).isTrue();
+
+        session2.start();
+
+        assertThat(starter2.hasLaunched()).isTrue();
+        assertThat(entry2.started()).isTrue();
+
+        runReturnTransition(starter2);
+
+        assertThat(exit2.started()).isTrue();
+
+        runReturnTransition(starter1);
+
+        assertThat(exit1.started()).isTrue();
+    }
+
+    private void runReturnTransition(FakeIntentStarter intentStarter) {
+        TransitionInfo info =
+                buildTransitionInfo(intentStarter.getToActivity(), intentStarter.getFromActivity());
+        mIOriginTransitions.runReturnTransition(intentStarter.getTransitionOfLastLaunch(), info);
+    }
+
+    private static TransitionInfo buildTransitionInfo(ComponentName from, ComponentName to) {
+        TransitionInfo info = new TransitionInfo(WindowManager.TRANSIT_OPEN, /* flags= */ 0);
+        TransitionInfo.Change c1 =
+                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+        c1.setMode(WindowManager.TRANSIT_OPEN);
+        c1.setActivityComponent(to);
+        TransitionInfo.Change c2 =
+                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+        c2.setMode(WindowManager.TRANSIT_CLOSE);
+        c2.setActivityComponent(from);
+        info.addChange(c2);
+        info.addChange(c1);
+        return info;
+    }
+
+    private static class FakeIntentStarter implements Predicate<RemoteTransition> {
+        private final ComponentName mFromActivity;
+        private final ComponentName mToActivity;
+        private final boolean mResult;
+
+        @Nullable private RemoteTransition mTransition;
+        private boolean mLaunched;
+
+        FakeIntentStarter(ComponentName from, ComponentName to) {
+            this(from, to, /* result= */ true);
+        }
+
+        FakeIntentStarter(ComponentName from, ComponentName to, boolean result) {
+            mFromActivity = from;
+            mToActivity = to;
+            mResult = result;
+        }
+
+        @Override
+        public boolean test(RemoteTransition transition) {
+            if (mResult) {
+                mLaunched = true;
+                mTransition = transition;
+                if (mTransition != null) {
+                    TransitionInfo info = buildTransitionInfo(mFromActivity, mToActivity);
+                    try {
+                        transition
+                                .getRemoteTransition()
+                                .startAnimation(
+                                        new Binder(),
+                                        info,
+                                        new SurfaceControl.Transaction(),
+                                        new FakeFinishCallback());
+                    } catch (RemoteException e) {
+
+                    }
+                }
+            }
+            return mResult;
+        }
+
+        @Nullable
+        public RemoteTransition getTransitionOfLastLaunch() {
+            return mTransition;
+        }
+
+        public ComponentName getFromActivity() {
+            return mFromActivity;
+        }
+
+        public ComponentName getToActivity() {
+            return mToActivity;
+        }
+
+        public boolean hasLaunched() {
+            return mLaunched;
+        }
+
+        public void reset() {
+            mTransition = null;
+            mLaunched = false;
+        }
+    }
+
+    private static class FakeIOriginTransitions extends IOriginTransitions.Stub {
+        private final Map<RemoteTransition, RemoteTransition> mRecords = new ArrayMap<>();
+
+        @Override
+        public RemoteTransition makeOriginTransition(
+                RemoteTransition launchTransition, RemoteTransition returnTransition) {
+            mRecords.put(launchTransition, returnTransition);
+            return launchTransition;
+        }
+
+        @Override
+        public void cancelOriginTransition(RemoteTransition originTransition) {
+            mRecords.remove(originTransition);
+        }
+
+        public void runReturnTransition(RemoteTransition originTransition, TransitionInfo info) {
+            RemoteTransition transition = mRecords.remove(originTransition);
+            try {
+                transition
+                        .getRemoteTransition()
+                        .startAnimation(
+                                new Binder(),
+                                info,
+                                new SurfaceControl.Transaction(),
+                                new FakeFinishCallback());
+            } catch (RemoteException e) {
+
+            }
+        }
+
+        public boolean hasPendingReturnTransitions() {
+            return !mRecords.isEmpty();
+        }
+    }
+
+    private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub {
+        @Override
+        public void onTransitionFinished(
+                WindowContainerTransaction wct, SurfaceControl.Transaction sct) {}
+    }
+
+    private static class FakeRemoteTransition extends IRemoteTransition.Stub {
+        private boolean mStarted;
+
+        @Override
+        public void startAnimation(
+                IBinder token,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback)
+                throws RemoteException {
+            mStarted = true;
+            finishCallback.onTransitionFinished(null, null);
+        }
+
+        @Override
+        public void mergeAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishCallback) {}
+
+        @Override
+        public void takeOverAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback,
+                WindowAnimationState[] states) {}
+
+        @Override
+        public void onTransitionConsumed(IBinder transition, boolean aborted) {}
+
+        public boolean started() {
+            return mStarted;
+        }
+
+        public void reset() {
+            mStarted = false;
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index a5f8057..20efea5 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -28,11 +28,11 @@
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButtonColors
 import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
 
 @Composable
 fun PlatformButton(
@@ -100,12 +100,7 @@
     @DrawableRes iconResource: Int,
     contentDescription: String?,
 ) {
-    IconButton(
-        modifier = modifier,
-        onClick = onClick,
-        enabled = enabled,
-        colors = colors,
-    ) {
+    IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) {
         Icon(
             painter = painterResource(id = iconResource),
             contentDescription = contentDescription,
@@ -118,7 +113,7 @@
 
 @Composable
 private fun filledButtonColors(): ButtonColors {
-    val colors = LocalAndroidColorScheme.current
+    val colors = MaterialTheme.colorScheme
     return ButtonDefaults.buttonColors(
         containerColor = colors.primary,
         contentColor = colors.onPrimary,
@@ -127,27 +122,22 @@
 
 @Composable
 private fun outlineButtonColors(): ButtonColors {
-    return ButtonDefaults.outlinedButtonColors(
-        contentColor = LocalAndroidColorScheme.current.onSurface,
-    )
+    return ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface)
 }
 
 @Composable
 private fun iconButtonColors(): IconButtonColors {
     return IconButtonDefaults.filledIconButtonColors(
-        contentColor = LocalAndroidColorScheme.current.onSurface,
+        contentColor = MaterialTheme.colorScheme.onSurface
     )
 }
 
 @Composable
 private fun outlineButtonBorder(): BorderStroke {
-    return BorderStroke(
-        width = 1.dp,
-        color = LocalAndroidColorScheme.current.primary,
-    )
+    return BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primary)
 }
 
 @Composable
 private fun textButtonColors(): ButtonColors {
-    return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary)
+    return ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary)
 }
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index bfeaf92..0f6e6a7 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
 import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -35,12 +36,7 @@
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-@Module(
-    includes =
-        [
-            LockscreenSceneBlueprintModule::class,
-        ],
-)
+@Module(includes = [LockscreenSceneBlueprintModule::class])
 interface LockscreenSceneModule {
 
     @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
@@ -51,9 +47,7 @@
         @Provides
         @SysUISingleton
         @KeyguardRootView
-        fun viewProvider(
-            configurator: Provider<KeyguardViewConfigurator>,
-        ): () -> View {
+        fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View {
             return { configurator.get().getKeyguardRootView() }
         }
 
@@ -67,10 +61,16 @@
         @Provides
         fun providesLockscreenContent(
             viewModelFactory: LockscreenContentViewModel.Factory,
+            notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
             blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
             clockInteractor: KeyguardClockInteractor,
         ): LockscreenContent {
-            return LockscreenContent(viewModelFactory, blueprints, clockInteractor)
+            return LockscreenContent(
+                viewModelFactory,
+                notificationScrimViewModelFactory,
+                blueprints,
+                clockInteractor,
+            )
         }
     }
 }
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 f4d1242..bcd3337 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
@@ -274,7 +274,7 @@
                                     if (layoutDirection == LayoutDirection.Rtl)
                                         screenWidth - offset.x
                                     else offset.x,
-                                    offset.y
+                                    offset.y,
                                 ) - contentOffset
                             val index = firstIndexAtOffset(gridState, adjustedOffset)
                             val key =
@@ -310,6 +310,9 @@
                                         it.changedToUp() || it.changedToUpIgnoreConsumed()
                                     }
                                 )
+
+                                // Reset state once touch ends.
+                                viewModel.onResetTouchState()
                             }
                         }
                     }
@@ -330,7 +333,7 @@
                                         if (layoutDirection == LayoutDirection.Rtl)
                                             screenWidth - offset.x
                                         else offset.x,
-                                        offset.y
+                                        offset.y,
                                     ) - it.positionInWindow() - contentOffset
                                 }
                             val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
@@ -344,14 +347,11 @@
                             }
                         }
                     }
-                },
+                }
     ) {
         AccessibilityContainer(viewModel) {
             if (!viewModel.isEditMode && isEmptyState) {
-                EmptyStateCta(
-                    contentPadding = contentPadding,
-                    viewModel = viewModel,
-                )
+                EmptyStateCta(contentPadding = contentPadding, viewModel = viewModel)
             } else {
                 val slideOffsetInPx =
                     with(LocalDensity.current) { Dimensions.SlideOffsetY.toPx().toInt() }
@@ -364,7 +364,7 @@
                         ) +
                             slideInVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                initialOffsetY = { -slideOffsetInPx }
+                                initialOffsetY = { -slideOffsetInPx },
                             ),
                     exit =
                         fadeOut(
@@ -372,7 +372,7 @@
                         ) +
                             slideOutVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                targetOffsetY = { -slideOffsetInPx }
+                                targetOffsetY = { -slideOffsetInPx },
                             ),
                     modifier = Modifier.fillMaxSize(),
                 ) {
@@ -389,7 +389,7 @@
                                     removeEnabled = removeButtonEnabled,
                                     offset =
                                         gridCoordinates?.let { it.positionInWindow() + offset },
-                                    containerToCheck = removeButtonCoordinates
+                                    containerToCheck = removeButtonCoordinates,
                                 )
                             },
                             gridState = gridState,
@@ -410,7 +410,7 @@
                 enter =
                     fadeIn(animationSpec = tween(durationMillis = 250, easing = LinearEasing)) +
                         slideInVertically(
-                            animationSpec = tween(durationMillis = 1000, easing = Emphasized),
+                            animationSpec = tween(durationMillis = 1000, easing = Emphasized)
                         ),
                 exit =
                     fadeOut(animationSpec = tween(durationMillis = 167, easing = LinearEasing)) +
@@ -434,7 +434,7 @@
                             viewModel.setSelectedKey(null)
                         }
                     },
-                    removeEnabled = removeButtonEnabled
+                    removeEnabled = removeButtonEnabled,
                 )
             }
         }
@@ -451,7 +451,7 @@
                 title = stringResource(id = R.string.dialog_title_to_allow_any_widget),
                 positiveButtonText = stringResource(id = R.string.button_text_to_open_settings),
                 onConfirm = viewModel::onEnableWidgetDialogConfirm,
-                onCancel = viewModel::onEnableWidgetDialogCancel
+                onCancel = viewModel::onEnableWidgetDialogCancel,
             )
 
             EnableWidgetDialog(
@@ -460,7 +460,7 @@
                 title = stringResource(id = R.string.work_mode_off_title),
                 positiveButtonText = stringResource(id = R.string.work_mode_turn_on),
                 onConfirm = viewModel::onEnableWorkProfileDialogConfirm,
-                onCancel = viewModel::onEnableWorkProfileDialogCancel
+                onCancel = viewModel::onEnableWorkProfileDialogCancel,
             )
         }
 
@@ -509,7 +509,7 @@
             imageVector = Icons.Outlined.Widgets,
             contentDescription = null,
             tint = colors.primary,
-            modifier = Modifier.size(32.dp)
+            modifier = Modifier.size(32.dp),
         )
         Spacer(modifier = Modifier.height(16.dp))
         Text(
@@ -527,7 +527,7 @@
                 Modifier.padding(horizontal = 26.dp, vertical = 16.dp)
                     .widthIn(min = 200.dp)
                     .heightIn(min = 56.dp),
-            onClick = { onButtonClicked() }
+            onClick = { onButtonClicked() },
         ) {
             Text(
                 stringResource(R.string.communal_widgets_disclaimer_button),
@@ -540,7 +540,7 @@
 @Composable
 private fun ObserveScrollEffect(
     gridState: LazyGridState,
-    communalViewModel: BaseCommunalViewModel
+    communalViewModel: BaseCommunalViewModel,
 ) {
 
     LaunchedEffect(gridState) {
@@ -667,7 +667,7 @@
             rememberGridDragDropState(
                 gridState = gridState,
                 contentListState = contentListState,
-                updateDragPositionForRemove = updateDragPositionForRemove
+                updateDragPositionForRemove = updateDragPositionForRemove,
             )
         gridModifier =
             gridModifier
@@ -677,7 +677,7 @@
                     LocalLayoutDirection.current,
                     screenWidth,
                     contentOffset,
-                    viewModel
+                    viewModel,
                 )
         // for widgets dropped from other activities
         val dragAndDropTargetState =
@@ -709,11 +709,7 @@
             contentType = { _, item -> item.key },
             span = { _, item -> GridItemSpan(item.size.span) },
         ) { index, item ->
-            val size =
-                SizeF(
-                    Dimensions.CardWidth.value,
-                    item.size.dp().value,
-                )
+            val size = SizeF(Dimensions.CardWidth.value, item.size.dp().value)
             val cardModifier = Modifier.requiredSize(width = size.width.dp, height = size.height.dp)
             if (viewModel.isEditMode && dragDropState != null) {
                 val selected = item.key == selectedKey.value
@@ -765,16 +761,13 @@
  * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available.
  */
 @Composable
-private fun EmptyStateCta(
-    contentPadding: PaddingValues,
-    viewModel: BaseCommunalViewModel,
-) {
+private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunalViewModel) {
     val colors = LocalAndroidColorScheme.current
     Card(
         modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
         border = BorderStroke(3.adjustedDp, colors.secondary),
-        shape = RoundedCornerShape(size = 80.adjustedDp)
+        shape = RoundedCornerShape(size = 80.adjustedDp),
     ) {
         Column(
             modifier = Modifier.fillMaxSize().padding(horizontal = 110.adjustedDp),
@@ -788,10 +781,7 @@
                 textAlign = TextAlign.Center,
                 color = colors.secondary,
             )
-            Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.Center,
-            ) {
+            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
                 Button(
                     modifier = Modifier.height(56.dp),
                     colors =
@@ -799,17 +789,13 @@
                             containerColor = colors.primary,
                             contentColor = colors.onPrimary,
                         ),
-                    onClick = {
-                        viewModel.onOpenWidgetEditor(
-                            shouldOpenWidgetPickerOnStart = true,
-                        )
-                    },
+                    onClick = { viewModel.onOpenWidgetEditor(shouldOpenWidgetPickerOnStart = true) },
                 ) {
                     Icon(
                         imageVector = Icons.Default.Add,
                         contentDescription =
                             stringResource(R.string.label_for_button_in_empty_state_cta),
-                        modifier = Modifier.size(24.dp)
+                        modifier = Modifier.size(24.dp),
                     )
                     Spacer(Modifier.width(ButtonDefaults.IconSpacing))
                     Text(
@@ -835,7 +821,7 @@
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
     setRemoveButtonCoordinates: (coordinates: LayoutCoordinates?) -> Unit,
     onOpenWidgetPicker: () -> Unit,
-    onEditDone: () -> Unit
+    onEditDone: () -> Unit,
 ) {
     if (!removeEnabled) {
         // Clear any existing coordinates when remove is not enabled.
@@ -844,7 +830,7 @@
     val removeButtonAlpha: Float by
         animateFloatAsState(
             targetValue = if (removeEnabled) 1f else 0.5f,
-            label = "RemoveButtonAlphaAnimation"
+            label = "RemoveButtonAlphaAnimation",
         )
 
     Box(
@@ -855,7 +841,7 @@
                     start = Dimensions.ToolbarPaddingHorizontal,
                     end = Dimensions.ToolbarPaddingHorizontal,
                 )
-                .onSizeChanged { setToolbarSize(it) },
+                .onSizeChanged { setToolbarSize(it) }
     ) {
         val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
         ToolbarButton(
@@ -864,16 +850,14 @@
             onClick = onOpenWidgetPicker,
         ) {
             Icon(Icons.Default.Add, null)
-            Text(
-                text = addWidgetText,
-            )
+            Text(text = addWidgetText)
         }
 
         AnimatedVisibility(
             modifier = Modifier.align(Alignment.Center),
             visible = removeEnabled,
             enter = fadeIn(),
-            exit = fadeOut()
+            exit = fadeOut(),
         ) {
             Button(
                 onClick = onRemoveClicked,
@@ -887,20 +871,18 @@
                             if (removeEnabled) {
                                 setRemoveButtonCoordinates(it)
                             }
-                        }
+                        },
             ) {
                 Row(
                     horizontalArrangement =
                         Arrangement.spacedBy(
                             ButtonDefaults.IconSpacing,
-                            Alignment.CenterHorizontally
+                            Alignment.CenterHorizontally,
                         ),
-                    verticalAlignment = Alignment.CenterVertically
+                    verticalAlignment = Alignment.CenterVertically,
                 ) {
                     Icon(Icons.Default.Close, contentDescription = null)
-                    Text(
-                        text = stringResource(R.string.button_to_remove_widget),
-                    )
+                    Text(text = stringResource(R.string.button_to_remove_widget))
                 }
             }
         }
@@ -911,9 +893,7 @@
             onClick = onEditDone,
         ) {
             Icon(Icons.Default.Check, contentDescription = null)
-            Text(
-                text = stringResource(R.string.hub_mode_editing_exit_button_text),
-            )
+            Text(text = stringResource(R.string.hub_mode_editing_exit_button_text))
         }
     }
 }
@@ -926,14 +906,14 @@
     isPrimary: Boolean = true,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
-    content: @Composable RowScope.() -> Unit
+    content: @Composable RowScope.() -> Unit,
 ) {
     val colors = LocalAndroidColorScheme.current
     AnimatedVisibility(
         visible = isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         Button(
             onClick = onClick,
@@ -943,7 +923,7 @@
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -954,21 +934,18 @@
         visible = !isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         OutlinedButton(
             onClick = onClick,
-            colors =
-                ButtonDefaults.outlinedButtonColors(
-                    contentColor = colors.onPrimaryContainer,
-                ),
+            colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.onPrimaryContainer),
             border = BorderStroke(width = 2.0.dp, color = colors.primary),
             contentPadding = Dimensions.ButtonPadding,
         ) {
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -1041,7 +1018,7 @@
                     size =
                         Size(width = size.width + padding * 2, height = size.height + padding * 2),
                     cornerRadius = CornerRadius(37.adjustedDp.toPx()),
-                    style = Stroke(width = 3.adjustedDp.toPx())
+                    style = Stroke(width = 3.adjustedDp.toPx()),
                 )
             }
     )
@@ -1061,7 +1038,7 @@
                 containerColor = colors.primary,
                 contentColor = colors.onPrimary,
             ),
-        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp)
+        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp),
     ) {
         Column(
             modifier =
@@ -1081,7 +1058,7 @@
                 style = MaterialTheme.typography.titleLarge,
                 fontSize = nonScalableTextSize(22.dp),
                 lineHeight = nonScalableTextSize(28.dp),
-                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F)
+                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F),
             )
             Spacer(modifier = Modifier.size(16.adjustedDp))
             Row(
@@ -1093,15 +1070,12 @@
                     LocalDensity provides
                         Density(
                             LocalDensity.current.density,
-                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f)
+                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f),
                         )
                 ) {
                     OutlinedButton(
                         modifier = Modifier.fillMaxHeight().weight(1F),
-                        colors =
-                            ButtonDefaults.buttonColors(
-                                contentColor = colors.onPrimary,
-                            ),
+                        colors = ButtonDefaults.buttonColors(contentColor = colors.onPrimary),
                         border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
                         onClick = viewModel::onDismissCtaTile,
                         contentPadding = PaddingValues(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1259,7 +1233,7 @@
                 visible = selected,
                 model = model,
                 widgetConfigurator = widgetConfigurator,
-                modifier = Modifier.align(Alignment.BottomEnd)
+                modifier = Modifier.align(Alignment.BottomEnd),
             )
         }
     }
@@ -1289,14 +1263,14 @@
                     containerColor = colors.primary,
                     contentColor = colors.onPrimary,
                     disabledContainerColor = Color.Transparent,
-                    disabledContentColor = Color.Transparent
+                    disabledContentColor = Color.Transparent,
                 ),
             onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } },
         ) {
             Icon(
                 imageVector = Icons.Outlined.Edit,
                 contentDescription = stringResource(id = R.string.edit_widget),
-                modifier = Modifier.padding(12.adjustedDp)
+                modifier = Modifier.padding(12.adjustedDp),
             )
         }
     }
@@ -1323,13 +1297,13 @@
                 .background(
                     color = MaterialTheme.colorScheme.surfaceVariant,
                     shape =
-                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
                 )
                 .clickable(
                     enabled = !viewModel.isEditMode,
                     interactionSource = null,
                     indication = null,
-                    onClick = viewModel::onOpenEnableWidgetDialog
+                    onClick = viewModel::onOpenEnableWidgetDialog,
                 ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1360,7 +1334,7 @@
         modifier =
             modifier.background(
                 color = MaterialTheme.colorScheme.surfaceVariant,
-                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
             ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1418,7 +1392,7 @@
                             MotionEvent.ACTION_MOVE,
                             change.position.x,
                             change.position.y,
-                            0
+                            0,
                         )
                     viewModel.mediaHost.hostView.dispatchTouchEvent(event)
                     event.recycle()
@@ -1429,12 +1403,12 @@
                 layoutParams =
                     FrameLayout.LayoutParams(
                         FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT
+                        FrameLayout.LayoutParams.MATCH_PARENT,
                     )
             }
             viewModel.mediaHost.hostView
         },
-        onReset = {}
+        onReset = {},
     )
 }
 
@@ -1462,7 +1436,7 @@
                             ) {
                                 viewModel.changeScene(
                                     CommunalScenes.Blank,
-                                    "closed by accessibility"
+                                    "closed by accessibility",
                                 )
                                 true
                             },
@@ -1471,7 +1445,7 @@
                             ) {
                                 viewModel.onOpenWidgetEditor()
                                 true
-                            }
+                            },
                         )
                 }
             }
@@ -1514,7 +1488,7 @@
         start = Dimensions.ToolbarPaddingHorizontal,
         end = Dimensions.ToolbarPaddingHorizontal,
         top = verticalPadding + toolbarHeight,
-        bottom = verticalPadding
+        bottom = verticalPadding,
     )
 }
 
@@ -1523,7 +1497,7 @@
     return with(LocalDensity.current) {
         ContentPaddingInPx(
             start = paddingValues.calculateStartPadding(LocalLayoutDirection.current).toPx(),
-            top = paddingValues.calculateTopPadding().toPx()
+            top = paddingValues.calculateTopPadding().toPx(),
         )
     }
 }
@@ -1536,7 +1510,7 @@
 fun isPointerWithinEnabledRemoveButton(
     removeEnabled: Boolean,
     offset: Offset?,
-    containerToCheck: LayoutCoordinates?
+    containerToCheck: LayoutCoordinates?,
 ): Boolean {
     if (!removeEnabled || offset == null || containerToCheck == null) {
         return false
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
index 0b96694..69ca0a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
 import kotlin.math.roundToInt
 
 /**
@@ -69,7 +68,7 @@
                 Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
                 propagateMinConstraints = true,
             ) {
-                val iconColor = LocalAndroidColorScheme.current.primary
+                val iconColor = MaterialTheme.colorScheme.primary
                 CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
             }
 
@@ -77,7 +76,7 @@
         }
 
         // Title.
-        val titleColor = LocalAndroidColorScheme.current.onSurface
+        val titleColor = MaterialTheme.colorScheme.onSurface
         CompositionLocalProvider(LocalContentColor provides titleColor) {
             ProvideTextStyle(
                 MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
@@ -88,7 +87,7 @@
         Spacer(Modifier.height(16.dp))
 
         // Content.
-        val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant
+        val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
         Box {
             CompositionLocalProvider(LocalContentColor provides contentColor) {
                 ProvideTextStyle(
@@ -169,7 +168,7 @@
                                 negative.width -
                                 positive.width -
                                 horizontalSpacing.roundToInt(),
-                            0
+                            0,
                         )
                     }
                 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index dbe7538..5c5514a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -30,6 +30,8 @@
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
 import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.notifications.ui.composable.NotificationLockscreenScrim
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
 
 /**
  * Renders the content of the lockscreen.
@@ -39,6 +41,7 @@
  */
 class LockscreenContent(
     private val viewModelFactory: LockscreenContentViewModel.Factory,
+    private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
     private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
     private val clockInteractor: KeyguardClockInteractor,
 ) {
@@ -47,10 +50,13 @@
     }
 
     @Composable
-    fun SceneScope.Content(
-        modifier: Modifier = Modifier,
-    ) {
-        val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() }
+    fun SceneScope.Content(modifier: Modifier = Modifier) {
+        val viewModel =
+            rememberViewModel("LockscreenContent-viewModel") { viewModelFactory.create() }
+        val notificationLockscreenScrimViewModel =
+            rememberViewModel("LockscreenContent-scrimViewModel") {
+                notificationScrimViewModelFactory.create()
+            }
         val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
         if (!isContentVisible) {
             // If the content isn't supposed to be visible, show a large empty box as it's needed
@@ -71,6 +77,9 @@
         }
 
         val blueprint = blueprintByBlueprintId[blueprintId] ?: return
-        with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) }
+        with(blueprint) {
+            Content(viewModel, modifier.sysuiResTag("keyguard_root_view"))
+            NotificationLockscreenScrim(notificationLockscreenScrimViewModel)
+        }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
new file mode 100644
index 0000000..4279be3
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
+import kotlinx.coroutines.launch
+
+/**
+ * A full-screen notifications scrim that is only visible after transitioning from Shade scene to
+ * Lockscreen Scene and ending user input, at which point it fades out, visually completing the
+ * transition.
+ */
+@Composable
+fun SceneScope.NotificationLockscreenScrim(
+    viewModel: NotificationLockscreenScrimViewModel,
+    modifier: Modifier = Modifier,
+) {
+    val coroutineScope = rememberCoroutineScope()
+    val shadeMode = viewModel.shadeMode.collectAsStateWithLifecycle()
+
+    // Important: Make sure that shouldShowScrimFadeOut() is checked the first time the Lockscreen
+    // scene is composed.
+    val useFadeOutOnComposition =
+        remember(shadeMode.value) {
+            layoutState.currentTransition?.let { currentTransition ->
+                shouldShowScrimFadeOut(currentTransition, shadeMode.value)
+            } ?: false
+        }
+
+    val alphaAnimatable = remember { Animatable(1f) }
+
+    LaunchedEffect(
+        alphaAnimatable,
+        layoutState.currentTransition,
+        useFadeOutOnComposition,
+        shadeMode,
+    ) {
+        val currentTransition = layoutState.currentTransition
+        if (
+            useFadeOutOnComposition &&
+                currentTransition != null &&
+                shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+                currentTransition.isUserInputOngoing
+        ) {
+            // keep scrim visible until user lifts their finger.
+            viewModel.setAlphaForLockscreenFadeIn(0f)
+            alphaAnimatable.snapTo(1f)
+        } else if (
+            useFadeOutOnComposition &&
+                (currentTransition == null ||
+                    (shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+                        !currentTransition.isUserInputOngoing))
+        ) {
+            // we no longer want to keep the scrim from fading out, so animate the scrim fade-out
+            // and pipe the progress to the view model as well, so NSSL can fade-in the stack in
+            // tandem.
+            viewModel.setAlphaForLockscreenFadeIn(0f)
+            coroutineScope.launch {
+                snapshotFlow { alphaAnimatable.value }
+                    .collect { viewModel.setAlphaForLockscreenFadeIn(1 - it) }
+            }
+            alphaAnimatable.animateTo(0f, tween())
+        } else {
+            // disable the scrim fade logic.
+            viewModel.setAlphaForLockscreenFadeIn(1f)
+            alphaAnimatable.snapTo(0f)
+        }
+    }
+
+    Box(
+        modifier
+            .fillMaxSize()
+            .element(Notifications.Elements.NotificationScrim)
+            .graphicsLayer { alpha = alphaAnimatable.value }
+            .background(MaterialTheme.colorScheme.surface)
+    )
+}
+
+private fun shouldShowScrimFadeOut(
+    currentTransition: TransitionState.Transition,
+    shadeMode: ShadeMode,
+): Boolean {
+    return shadeMode == ShadeMode.Single &&
+        currentTransition.isInitiatedByUserInput &&
+        (currentTransition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) ||
+            currentTransition.isTransitioning(from = Scenes.Bouncer, to = Scenes.Lockscreen))
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index fe4a65b..2066c93 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -86,6 +86,8 @@
 import com.android.compose.animation.scene.LowestZIndexContentPicker
 import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
 import com.android.systemui.res.R
@@ -101,7 +103,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 object Notifications {
@@ -251,6 +253,7 @@
         NotificationPlaceholder(
             stackScrollView = stackScrollView,
             viewModel = viewModel,
+            useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) },
             modifier = Modifier.fillMaxSize(),
         )
         HeadsUpNotificationSpace(
@@ -363,7 +366,6 @@
         snapshotFlow { syntheticScroll.value }
             .collect { delta ->
                 scrollNotificationStack(
-                    scope = coroutineScope,
                     delta = delta,
                     animate = false,
                     scrimOffset = scrimOffset,
@@ -383,7 +385,6 @@
                 // composed at least once), and our remote input row overlaps with the ime bounds.
                 if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
                     scrollNotificationStack(
-                        scope = coroutineScope,
                         delta = remoteInputRowBottom - imeTopValue,
                         animate = true,
                         scrimOffset = scrimOffset,
@@ -450,7 +451,10 @@
                                 scrimCornerRadius,
                                 screenCornerRadius,
                                 { expansionFraction },
-                                shouldPunchHoleBehindScrim,
+                                shouldAnimateScrimCornerRadius(
+                                    layoutState,
+                                    shouldPunchHoleBehindScrim,
+                                ),
                             )
                             .let { scrimRounding.value.toRoundedCornerShape(it) }
                     clip = true
@@ -514,6 +518,9 @@
                 NotificationPlaceholder(
                     stackScrollView = stackScrollView,
                     viewModel = viewModel,
+                    useStackBounds = {
+                        !shouldUseLockscreenStackBounds(layoutState.transitionState)
+                    },
                     modifier =
                         Modifier.notificationStackHeight(
                                 view = stackScrollView,
@@ -600,6 +607,7 @@
 private fun SceneScope.NotificationPlaceholder(
     stackScrollView: NotificationScrollView,
     viewModel: NotificationsPlaceholderViewModel,
+    useStackBounds: () -> Boolean,
     modifier: Modifier = Modifier,
 ) {
     Box(
@@ -609,21 +617,26 @@
                 .debugBackground(viewModel, DEBUG_STACK_COLOR)
                 .onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } }
                 .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                    val positionInWindow = coordinates.positionInWindow()
-                    debugLog(viewModel) {
-                        "STACK onGloballyPositioned:" +
-                            " size=${coordinates.size}" +
-                            " position=$positionInWindow" +
-                            " bounds=${coordinates.boundsInWindow()}"
+                    // This element is opted out of the shared element system, so there can be
+                    // multiple instances of it during a transition. Thus we need to determine which
+                    // instance should feed its bounds to NSSL to avoid providing conflicting values
+                    val useBounds = useStackBounds()
+                    if (useBounds) {
+                        // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top won't
+                        val positionInWindow = coordinates.positionInWindow()
+                        debugLog(viewModel) {
+                            "STACK onGloballyPositioned:" +
+                                " size=${coordinates.size}" +
+                                " position=$positionInWindow" +
+                                " bounds=${coordinates.boundsInWindow()}"
+                        }
+                        stackScrollView.setStackTop(positionInWindow.y)
                     }
-                    // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
-                    stackScrollView.setStackTop(positionInWindow.y)
                 }
     )
 }
 
 private suspend fun scrollNotificationStack(
-    scope: CoroutineScope,
     delta: Float,
     animate: Boolean,
     scrimOffset: Animatable<Float, AnimationVector1D>,
@@ -638,7 +651,7 @@
             if (animate) {
                 // launch a new coroutine for the remainder animation so that it doesn't suspend the
                 // scrim animation, allowing both to play simultaneously.
-                scope.launch { scrollState.animateScrollTo(remainingDelta) }
+                coroutineScope { launch { scrollState.animateScrollTo(remainingDelta) } }
             } else {
                 scrollState.scrollTo(remainingDelta)
             }
@@ -658,6 +671,18 @@
     }
 }
 
+private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean {
+    return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen
+}
+
+private fun shouldAnimateScrimCornerRadius(
+    state: SceneTransitionLayoutState,
+    shouldPunchHoleBehindScrim: Boolean,
+): Boolean {
+    return shouldPunchHoleBehindScrim ||
+        state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
+}
+
 private fun calculateCornerRadius(
     scrimCornerRadius: Dp,
     screenCornerRadius: Dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index df101c5..dc9e267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -126,17 +126,18 @@
                     awaitFirstDown(false)
                     viewModel.onSceneContainerUserInputStarted()
                 }
-            },
+            }
     ) {
         SceneTransitionLayout(
             state = state,
             modifier = modifier.fillMaxSize(),
             swipeSourceDetector = viewModel.edgeDetector,
+            gestureFilter = viewModel::shouldFilterGesture,
         ) {
             sceneByKey.forEach { (sceneKey, scene) ->
                 scene(
                     key = sceneKey,
-                    userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap())
+                    userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()),
                 ) {
                     // Activate the scene.
                     LaunchedEffect(scene) { scene.activate() }
@@ -144,7 +145,7 @@
                     // Render the scene.
                     with(scene) {
                         this@scene.Content(
-                            modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
+                            modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
                         )
                     }
                 }
@@ -152,7 +153,7 @@
             overlayByKey.forEach { (overlayKey, overlay) ->
                 overlay(
                     key = overlayKey,
-                    userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap())
+                    userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()),
                 ) {
                     // Activate the overlay.
                     LaunchedEffect(overlay) { overlay.activate() }
@@ -164,12 +165,7 @@
         }
 
         BottomRightCornerRibbon(
-            content = {
-                Text(
-                    text = "flexi\uD83E\uDD43",
-                    color = Color.White,
-                )
-            },
+            content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
             modifier = Modifier.align(Alignment.BottomEnd),
         )
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 58fbf43..303a6f0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -82,18 +82,35 @@
         sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
     }
     from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
+    from(Scenes.Shade, to = Scenes.Lockscreen) {
+        reversed { lockscreenToShadeTransition() }
+        sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false)
+    }
 
     // Overlay transitions
 
     to(Overlays.NotificationsShade) { toNotificationsShadeTransition() }
     to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() }
-    from(Overlays.NotificationsShade, Overlays.QuickSettingsShade) {
+    from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) {
         notificationsShadeToQuickSettingsShadeTransition()
     }
+    from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+        toNotificationsShadeTransition(durationScale = 0.9)
+    }
+    from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+        toQuickSettingsShadeTransition(durationScale = 0.9)
+    }
+    from(Scenes.Lockscreen, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+        toNotificationsShadeTransition(durationScale = 0.9)
+    }
+    from(Scenes.Lockscreen, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+        toQuickSettingsShadeTransition(durationScale = 0.9)
+    }
 
     // Scene overscroll
 
     overscrollDisabled(Scenes.Gone, Orientation.Vertical)
+    overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical)
     overscroll(Scenes.Bouncer, Orientation.Vertical) {
         translate(Bouncer.Elements.Content, y = { absoluteDistance })
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
index ac54896..4c0efd2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
@@ -4,13 +4,19 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
 import com.android.systemui.bouncer.ui.composable.Bouncer
 
 const val FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION = 0.5f
+const val FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f
 
 fun TransitionBuilder.lockscreenToBouncerTransition() {
     spec = tween(durationMillis = 500)
 
+    distance = UserActionDistance { fromSceneSize, _ ->
+        fromSceneSize.height * FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION
+    }
+
     translate(Bouncer.Elements.Content, y = 300.dp)
     fractionRange(end = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) {
         fade(Bouncer.Elements.Background)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 7f2ee2a..db0fe3e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -296,7 +296,7 @@
 
     val shouldPunchHoleBehindScrim =
         layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
-            layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
+            layoutState.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
     // Media is visible and we are in landscape on a small height screen
     val mediaInRow = isMediaVisible && isLandscape()
     val mediaOffset by
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f38a310..9891025 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -123,6 +123,10 @@
         overSlop: Float,
         pointersDown: Int,
     ): DragController {
+        if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
+            return NoOpDragController
+        }
+
         if (overSlop == 0f) {
             val oldDragController = dragController
             check(oldDragController != null && oldDragController.isDrivingTransition) {
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 cec8883..6e89814 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
@@ -47,6 +47,9 @@
  * @param state the state of this layout.
  * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
  *   if any.
+ * @param gestureFilter decides whether a drag gesture that started at the given start position
+ *   should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
+ *   returns `false`, the drag gesture will be handled.
  * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
  *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
  * @param builder the configuration of the different scenes and overlays of this layout.
@@ -57,6 +60,7 @@
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
+    gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
     builder: SceneTransitionLayoutScope.() -> Unit,
 ) {
@@ -65,6 +69,7 @@
         modifier,
         swipeSourceDetector,
         swipeDetector,
+        gestureFilter,
         transitionInterceptionThreshold,
         onLayoutImpl = null,
         builder,
@@ -616,6 +621,7 @@
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
+    gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
     transitionInterceptionThreshold: Float = 0f,
     onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
     builder: SceneTransitionLayoutScope.() -> Unit,
@@ -632,6 +638,7 @@
                 transitionInterceptionThreshold = transitionInterceptionThreshold,
                 builder = builder,
                 animationScope = animationScope,
+                gestureFilter = gestureFilter,
             )
             .also { onLayoutImpl?.invoke(it) }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 65c4043..9e7be37 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.ApproachLayoutModifierNode
 import androidx.compose.ui.layout.ApproachMeasureScope
 import androidx.compose.ui.layout.LookaheadScope
@@ -70,6 +71,7 @@
      * animations.
      */
     internal val animationScope: CoroutineScope,
+    internal val gestureFilter: (startedPosition: Offset) -> Boolean,
 ) {
     /**
      * The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index 54ee783..f758102 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,6 +17,7 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.PointerInputChange
 
 /** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -31,6 +32,8 @@
 
 val DefaultSwipeDetector = PassthroughSwipeDetector()
 
+val DefaultGestureFilter = { _: Offset -> false }
+
 /** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
 class PassthroughSwipeDetector : SwipeDetector {
     override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index fca92ca..b64b8be 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -108,6 +108,8 @@
 
         val transitionInterceptionThreshold = 0.05f
 
+        var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
+
         private val layoutImpl =
             SceneTransitionLayoutImpl(
                     state = layoutState,
@@ -120,6 +122,7 @@
                     // Use testScope and not backgroundScope here because backgroundScope does not
                     // work well with advanceUntilIdle(), which is used by some tests.
                     animationScope = testScope,
+                    gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
                 )
                 .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
 
@@ -317,6 +320,13 @@
     }
 
     @Test
+    fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
+        gestureFilter = { _ -> true }
+        onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
+        assertIdle(currentScene = SceneA)
+    }
+
+    @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
         val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index c163c6f..0490a26 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -350,7 +350,7 @@
         testScope.runTest {
             underTest.performDotFeedback(null)
 
-            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR)
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE)
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index e25c1a7..d5020a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -109,7 +109,9 @@
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
         underTest.start()
         kosmos.communalSceneRepository.setTransitionState(sceneTransitions)
-        testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) }
+        testScope.launch {
+            keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN, testSetup = true)
+        }
     }
 
     /** Transition from blank to glanceable hub. This is the default case. */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index ab33269..d7fe263 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.education.domain.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationManager
 import android.content.applicationContext
-import android.widget.Toast
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -34,11 +34,13 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,6 +53,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -63,10 +66,12 @@
     private val minDurationForNextEdu =
         KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
     private lateinit var underTest: ContextualEduUiCoordinator
-    @Mock private lateinit var toast: Toast
+    @Mock private lateinit var dialog: Dialog
     @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
     @get:Rule val mockitoRule = MockitoJUnit.rule()
     private var toastContent = ""
+    private val timeoutMillis = 3500L
 
     @Before
     fun setUp() {
@@ -75,30 +80,35 @@
             interactor.updateTouchpadFirstConnectionTime()
         }
 
+        whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any()))
+            .thenReturn(timeoutMillis.toInt())
+
         val viewModel =
             ContextualEduViewModel(
                 kosmos.applicationContext.resources,
-                kosmos.keyboardTouchpadEduInteractor
+                kosmos.keyboardTouchpadEduInteractor,
+                accessibilityManagerWrapper,
             )
+
         underTest =
             ContextualEduUiCoordinator(
                 kosmos.applicationCoroutineScope,
                 viewModel,
                 kosmos.applicationContext,
                 notificationManager
-            ) { content ->
-                toastContent = content
-                toast
+            ) { model ->
+                toastContent = model.message
+                dialog
             }
         underTest.start()
         kosmos.keyboardTouchpadEduInteractor.start()
     }
 
     @Test
-    fun showToastOnNewEdu() =
+    fun showDialogOnNewEdu() =
         testScope.runTest {
             triggerEducation(BACK)
-            verify(toast).show()
+            verify(dialog).show()
         }
 
     @Test
@@ -111,6 +121,14 @@
         }
 
     @Test
+    fun dismissDialogAfterTimeout() =
+        testScope.runTest {
+            triggerEducation(BACK)
+            advanceTimeBy(timeoutMillis + 1)
+            verify(dialog).dismiss()
+        }
+
+    @Test
     fun verifyBackEduToastContent() =
         testScope.runTest {
             triggerEducation(BACK)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index a08fbbf..fa304c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -198,6 +198,13 @@
     @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     fun testTransitionToGlanceableHubOnWake() =
         testScope.runTest {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            reset(transitionRepository)
+
             whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
             kosmos.setCommunalAvailable(true)
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index ba68917..d97909a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -19,17 +19,22 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.DismissAction
 import com.android.systemui.keyguard.shared.model.KeyguardDone
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -44,6 +49,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -76,12 +82,12 @@
                 transitionInteractor = kosmos.keyguardTransitionInteractor,
                 dismissInteractor = dismissInteractor,
                 applicationScope = testScope.backgroundScope,
-                sceneInteractor = { kosmos.sceneInteractor },
                 deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor },
                 powerInteractor = kosmos.powerInteractor,
                 alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
                 shadeInteractor = { kosmos.shadeInteractor },
                 keyguardInteractor = { kosmos.keyguardInteractor },
+                sceneInteractor = { kosmos.sceneInteractor },
             )
     }
 
@@ -180,7 +186,11 @@
             )
             assertThat(executeDismissAction).isNull()
 
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
             kosmos.setSceneTransition(Idle(Scenes.Gone))
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
 
             assertThat(executeDismissAction).isNotNull()
         }
@@ -303,4 +313,78 @@
             underTest.setKeyguardDone(KeyguardDone.IMMEDIATE)
             assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE)
         }
+
+    @Test
+    @EnableSceneContainer
+    fun dismissAction_executesBeforeItsReset_sceneContainerOn_swipeAuth_fromQsScene() =
+        testScope.runTest {
+            val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(currentScene!!)
+                )
+            kosmos.sceneInteractor.setTransitionState(transitionState)
+            val executeDismissAction by collectLastValue(underTest.executeDismissAction)
+            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.None
+            )
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+            assertThat(canSwipeToEnter).isTrue()
+            kosmos.sceneInteractor.changeScene(Scenes.QuickSettings, "")
+            transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings)
+            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            val dismissAction =
+                DismissAction.RunImmediately(
+                    onDismissAction = { KeyguardDone.LATER },
+                    onCancelAction = {},
+                    message = "message",
+                    willAnimateOnLockscreen = true,
+                )
+            underTest.setDismissAction(dismissAction)
+            // Should still be null because the transition to Gone has not yet happened.
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            transitionState.value =
+                ObservableTransitionState.Transition.ChangeScene(
+                    fromScene = Scenes.QuickSettings,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.QuickSettings),
+                    currentOverlays = emptySet(),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            runCurrent()
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            transitionState.value =
+                ObservableTransitionState.Transition.ChangeScene(
+                    fromScene = Scenes.QuickSettings,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.Gone),
+                    currentOverlays = emptySet(),
+                    progress = flowOf(1f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            runCurrent()
+            assertThat(executeDismissAction).isNotNull()
+            assertThat(resetDismissAction).isNull()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
index 129752e..aab46d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.testKosmos
 import org.junit.Before
 import org.junit.Test
@@ -44,6 +45,7 @@
             KeyguardBlueprintViewModel(
                 handler = kosmos.fakeExecutorHandler,
                 keyguardBlueprintInteractor = keyguardBlueprintInteractor,
+                keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index e6ea64f..d0da2e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -89,9 +89,12 @@
     }
 
     @Test
-    fun lockscreenFadeOut() =
+    fun lockscreenFadeOut_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
             repository.sendTransitionSteps(
                 steps =
                     listOf(
@@ -104,10 +107,34 @@
                     ),
                 testScope = testScope,
             )
-            // Only 5 values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
             assertThat(values.size).isEqualTo(5)
-            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+            assertThat(values[0]).isEqualTo(1f)
+            assertThat(values[1]).isEqualTo(1f)
+            assertThat(values[2]).isIn(Range.open(0f, 1f))
+            assertThat(values[3]).isIn(Range.open(0f, 1f))
+            assertThat(values[4]).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenFadeOut_shadeExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.1f),
+                        step(.4f),
+                        step(.7f), // ...up to here
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
+            values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
     @Test
@@ -115,7 +142,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -138,7 +165,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -171,7 +198,7 @@
                     listOf(
                         step(0f, TransitionState.STARTED),
                         step(.5f),
-                        step(1f, TransitionState.FINISHED)
+                        step(1f, TransitionState.FINISHED),
                     ),
                 testScope = testScope,
             )
@@ -228,7 +255,7 @@
             to = KeyguardState.OCCLUDED,
             value = value,
             transitionState = state,
-            ownerName = "LockscreenToOccludedTransitionViewModelTest"
+            ownerName = "LockscreenToOccludedTransitionViewModelTest",
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..0b7a38e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.platform.test.flag.junit.FlagsParameterization
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class OffToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    lateinit var underTest: OffToLockscreenTransitionViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.offToLockscreenTransitionViewModel
+    }
+
+    @Test
+    fun lockscreenAlpha() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.lockscreenAlpha)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            assertThat(alpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.66f))
+            assertThat(alpha).isIn(Range.open(.1f, .9f))
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(alpha).isEqualTo(1f)
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.OFF,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "OffToLockscreenTransitionViewModelTest",
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
index 3e3aa4f..e12c67b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
@@ -18,7 +18,7 @@
 import com.android.wm.shell.recents.RecentTasks
 import com.android.wm.shell.shared.GroupedRecentTaskInfo
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import java.util.function.Consumer
@@ -268,7 +268,7 @@
         GroupedRecentTaskInfo.forSplitTasks(
             createTaskInfo(taskId1, userId1, isVisible),
             createTaskInfo(taskId2, userId2, isVisible),
-            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_50_50)
+            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_2_50_50)
         )
 
     private fun createTaskInfo(taskId: Int, userId: Int, isVisible: Boolean = false) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 88a1df1..ada2138 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -16,46 +16,130 @@
 
 package com.android.systemui.notifications.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 @EnableSceneContainer
+@EnableFlags(DualShade.FLAG_NAME)
 class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val sceneInteractor = kosmos.sceneInteractor
 
-    private val underTest = kosmos.notificationsShadeOverlayContentViewModel
+    private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
+
+    @Before
+    fun setUp() {
+        kosmos.sceneContainerStartable.start()
+        underTest.activateIn(testScope)
+    }
 
     @Test
     fun onScrimClicked_hidesShade() =
         testScope.runTest {
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-            sceneInteractor.showOverlay(
-                overlay = Overlays.NotificationsShade,
-                loggingReason = "test",
-            )
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
             assertThat(currentOverlays).contains(Overlays.NotificationsShade)
 
             underTest.onScrimClicked()
 
             assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
         }
+
+    @Test
+    fun deviceLocked_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            unlockDevice()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            lockDevice()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    @Test
+    fun bouncerShown_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            lockDevice()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            sceneInteractor.changeScene(Scenes.Bouncer, "test")
+            runCurrent()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    @Test
+    fun shadeNotTouchable_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
+            assertThat(isShadeTouchable).isTrue()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            lockDevice()
+            assertThat(isShadeTouchable).isFalse()
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    private fun TestScope.lockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAsleepForTest()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+    }
+
+    private suspend fun TestScope.unlockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAwakeForTest()
+        runCurrent()
+        assertThat(
+                kosmos.authenticationInteractor.authenticate(
+                    FakeAuthenticationRepository.DEFAULT_PIN
+                )
+            )
+            .isEqualTo(AuthenticationResult.SUCCEEDED)
+
+        assertThat(currentScene).isEqualTo(Scenes.Gone)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index 2580ac2..7798f46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.FLAG_QS_NEW_TILES;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
@@ -21,11 +23,16 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.service.quicksettings.Tile;
 import android.testing.UiThreadTest;
 import android.widget.ImageView;
@@ -47,7 +54,6 @@
 @UiThreadTest
 @SmallTest
 public class QSIconViewImplTest extends SysuiTestCase {
-
     private QSIconViewImpl mIconView;
 
     @Before
@@ -106,6 +112,34 @@
         verify(iv).setImageTintList(argThat(stateList -> stateList.getColors()[0] == desiredColor));
     }
 
+
+    @EnableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOn_immediatelyLoadsAll3TintColors() {
+        Context ctx = spy(mContext);
+
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+
+        iconView.getColor(new State()); // this should not increase the call count
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+    }
+
+    @DisableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOff_loadsOneTintColorAfterIconColorIsRead() {
+        Context ctx = spy(mContext);
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, never()).obtainStyledAttributes(any()); // none of the colors are preloaded
+
+        iconView.getColor(new State());
+
+        verify(ctx, times(1)).obtainStyledAttributes(any());
+    }
+
     @Test
     public void testStateSetCorrectly_toString() {
         ImageView iv = mock(ImageView.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
index 620e90d..d32ba47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -17,13 +17,17 @@
 package com.android.systemui.qs.tiles.impl.internet.domain
 
 import android.graphics.drawable.TestStubDrawable
+import android.os.fakeExecutorHandler
 import android.widget.Switch
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
@@ -31,6 +35,9 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,25 +46,93 @@
 class InternetTileMapperTest : SysuiTestCase() {
     private val kosmos = Kosmos()
     private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val handler = kosmos.fakeExecutorHandler
     private val mapper by lazy {
         InternetTileMapper(
             context.orCreateTestableResources
                 .apply {
                     addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+                    addOverride(R.drawable.ic_satellite_connected_2, TestStubDrawable())
                     addOverride(wifiRes, TestStubDrawable())
                 }
                 .resources,
             context.theme,
-            context
+            context,
+            handler,
         )
     }
 
     @Test
-    fun withActiveModel_mappedStateMatchesDataModel() {
+    fun withActiveCellularModel_mappedStateMatchesDataModel() {
         val inputModel =
             InternetTileModel.Active(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
-                iconId = wifiRes,
+                icon = InternetTileIconModel.Cellular(3),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_internet_label),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val signalDrawable = SignalDrawable(context, handler)
+        signalDrawable.setLevel(3)
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_networks_available),
+                Icon.Loaded(signalDrawable, null),
+                null,
+                context.getString(R.string.quick_settings_internet_label),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveSatelliteModel_mappedStateMatchesDataModel() {
+        val inputIcon =
+            SignalIconModel.Satellite(
+                3,
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_connected_2,
+                    contentDescription =
+                        ContentDescription.Resource(
+                            R.string.accessibility_status_bar_satellite_good_connection
+                        ),
+                ),
+            )
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.Satellite(inputIcon.icon),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(
+                        R.string.accessibility_status_bar_satellite_good_connection
+                    ),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedSatIcon = SatelliteIconModel.fromSignalStrength(3)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                inputModel.secondaryLabel.loadText(context).toString(),
+                Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null),
+                expectedSatIcon.res,
+                expectedSatIcon.contentDescription.loadContentDescription(context).toString(),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveWifiModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.ResourceId(wifiRes),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_internet_label),
@@ -71,7 +146,7 @@
                 context.getString(R.string.quick_settings_networks_available),
                 Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
                 wifiRes,
-                context.getString(R.string.quick_settings_internet_label)
+                context.getString(R.string.quick_settings_internet_label),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -81,7 +156,7 @@
         val inputModel =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
@@ -95,10 +170,10 @@
                 context.getString(R.string.quick_settings_networks_unavailable),
                 Icon.Loaded(
                     context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
-                    contentDescription = null
+                    contentDescription = null,
                 ),
                 R.drawable.ic_qs_no_internet_unavailable,
-                context.getString(R.string.quick_settings_networks_unavailable)
+                context.getString(R.string.quick_settings_networks_unavailable),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -107,7 +182,7 @@
         activationState: QSTileState.ActivationState,
         secondaryLabel: String,
         icon: Icon,
-        iconRes: Int,
+        iconRes: Int? = null,
         contentDescription: String,
     ): QSTileState {
         val label = context.getString(R.string.quick_settings_internet_label)
@@ -120,13 +195,13 @@
             setOf(
                 QSTileState.UserAction.CLICK,
                 QSTileState.UserAction.TOGGLE_CLICK,
-                QSTileState.UserAction.LONG_CLICK
+                QSTileState.UserAction.LONG_CLICK,
             ),
             contentDescription,
             null,
             QSTileState.SideViewIcon.Chevron,
             QSTileState.EnabledState.ENABLED,
-            Switch::class.qualifiedName
+            Switch::class.qualifiedName,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
index 5a45060..5259aa8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -18,14 +18,12 @@
 
 import android.graphics.drawable.TestStubDrawable
 import android.os.UserHandle
-import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.coroutines.collectLastValue
@@ -49,6 +47,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
@@ -60,9 +59,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -144,7 +141,6 @@
         underTest =
             InternetTileDataInteractor(
                 context,
-                testScope.coroutineContext,
                 testScope.backgroundScope,
                 airplaneModeRepository,
                 connectivityRepository,
@@ -164,9 +160,11 @@
 
             connectivityRepository.defaultConnections.value = DefaultConnectionModel()
 
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable)
             assertThat(latest?.secondaryLabel)
                 .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
         }
 
     @Test
@@ -183,11 +181,8 @@
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
 
-            val networkModel =
-                WifiNetworkModel.Active.of(
-                    level = 4,
-                    ssid = "test ssid",
-                )
+            val networkModel = WifiNetworkModel.Active.of(level = 4, ssid = "test ssid")
+
             val wifiIcon =
                 WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
                     as WifiIcon.Visible
@@ -198,12 +193,9 @@
 
             assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
             assertThat(latest?.secondaryLabel).isNull()
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
 
-            val actualIcon = latest?.icon
-            assertThat(actualIcon).isEqualTo(expectedIcon)
-            assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo("$internet,test ssid")
             val expectedSd = wifiIcon.contentDescription
@@ -229,8 +221,7 @@
             wifiRepository.setIsWifiDefault(true)
             wifiRepository.setWifiNetwork(networkModel)
 
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .doesNotContain(
@@ -249,9 +240,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_tablet
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -271,9 +261,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_laptop
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -293,10 +282,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_watch
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -315,10 +304,7 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
-                    null
-                )
+                InternetTileIconModel.ResourceId(com.android.settingslib.R.drawable.ic_hotspot_auto)
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -336,9 +322,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -358,9 +343,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -380,10 +364,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -426,8 +410,9 @@
             assertThat(latest?.secondaryLabel).isNull()
             assertThat(latest?.secondaryTitle)
                 .isEqualTo(context.getString(R.string.quick_settings_networks_available))
-            assertThat(latest?.icon).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_available)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             val expectedCd =
                 "$internet,${context.getString(R.string.quick_settings_networks_available)}"
@@ -435,54 +420,19 @@
                 .isEqualTo(expectedCd)
         }
 
-    /**
-     * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the
-     * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler()
-     * on the mentioned context. Since that context does not have a looper assigned to it, the
-     * handler instantiation will throw a RuntimeException.
-     *
-     * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So
-     *   either we should make Robolectric behave similar to the device test, or change this test to
-     *   look for a different signal than the exception, when run by Robolectric. For now we just
-     *   assume the test is not Robolectric.
-     */
-    @Test(expected = java.lang.RuntimeException::class)
-    fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() =
-        testScope.runTest {
-            assumeFalse(isRobolectricTest())
-
-            collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
-
-            connectivityRepository.setMobileConnected()
-            mobileConnectionsRepository.mobileIsDefault.value = true
-            mobileConnectionRepository.apply {
-                setAllLevels(3)
-                setAllRoaming(false)
-                networkName.value = NetworkNameModel.Default("test network")
-            }
-
-            runCurrent()
-        }
-
-    /**
-     * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the
-     * problem this test solves. The solution here is to assign a looper to the context via
-     * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for
-     * creating the SignalDrawable.
-     */
-    @TestableLooper.RunWithLooper
     @Test
-    fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() =
+    fun mobileDefault_usesNetworkNameAndIcon() =
         testScope.runTest {
             val latest by
                 collectLastValue(
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
+            val iconLevel = 3
 
             connectivityRepository.setMobileConnected()
             mobileConnectionsRepository.mobileIsDefault.value = true
             mobileConnectionRepository.apply {
-                setAllLevels(3)
+                setAllLevels(iconLevel)
                 setAllRoaming(false)
                 networkName.value = NetworkNameModel.Default("test network")
             }
@@ -491,8 +441,9 @@
             assertThat(latest?.secondaryTitle).isNotNull()
             assertThat(latest?.secondaryTitle.toString()).contains("test network")
             assertThat(latest?.secondaryLabel).isNull()
-            assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
-            assertThat(latest?.iconId).isNull()
+            val expectedIcon = InternetTileIconModel.Cellular(iconLevel)
+
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryTitle.toString())
             assertThat(latest?.contentDescription.loadContentDescription(context))
@@ -513,8 +464,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet_fully)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -534,8 +485,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -543,11 +494,7 @@
 
     private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
         val networkModel =
-            WifiNetworkModel.Active.of(
-                level = 4,
-                ssid = "test ssid",
-                hotspotDeviceType = hotspot,
-            )
+            WifiNetworkModel.Active.of(level = 4, ssid = "test ssid", hotspotDeviceType = hotspot)
 
         connectivityRepository.setWifiConnected()
         wifiRepository.setIsWifiDefault(true)
@@ -560,7 +507,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index abd1e2c..f32894d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -16,45 +16,129 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 @EnableSceneContainer
+@EnableFlags(DualShade.FLAG_NAME)
 class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val sceneInteractor = kosmos.sceneInteractor
 
-    private val underTest = kosmos.quickSettingsShadeOverlayContentViewModel
+    private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
+
+    @Before
+    fun setUp() {
+        kosmos.sceneContainerStartable.start()
+        underTest.activateIn(testScope)
+    }
 
     @Test
     fun onScrimClicked_hidesShade() =
         testScope.runTest {
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-            sceneInteractor.showOverlay(
-                overlay = Overlays.QuickSettingsShade,
-                loggingReason = "test",
-            )
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
             assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
 
             underTest.onScrimClicked()
 
             assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
         }
+
+    @Test
+    fun deviceLocked_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            unlockDevice()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            lockDevice()
+
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    fun bouncerShown_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            lockDevice()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            sceneInteractor.changeScene(Scenes.Bouncer, "test")
+            runCurrent()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
+        }
+
+    @Test
+    fun shadeNotTouchable_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
+            assertThat(isShadeTouchable).isTrue()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            lockDevice()
+            assertThat(isShadeTouchable).isFalse()
+            assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
+        }
+
+    private fun TestScope.lockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAsleepForTest()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+    }
+
+    private suspend fun TestScope.unlockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAwakeForTest()
+        runCurrent()
+        assertThat(
+                kosmos.authenticationInteractor.authenticate(
+                    FakeAuthenticationRepository.DEFAULT_PIN
+                )
+            )
+            .isEqualTo(AuthenticationResult.SUCCEEDED)
+
+        assertThat(currentScene).isEqualTo(Scenes.Gone)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
similarity index 88%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
index 57cfe1b..3e5dee6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
@@ -47,7 +47,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() {
+class IssueRecordingServiceSessionTest : SysuiTestCase() {
 
     private val kosmos = Kosmos().also { it.testCase = this }
     private val bgExecutor = kosmos.fakeExecutor
@@ -61,13 +61,13 @@
     private val notificationManager = mock<NotificationManager>()
     private val panelInteractor = mock<PanelInteractor>()
 
-    private lateinit var underTest: IssueRecordingServiceCommandHandler
+    private lateinit var underTest: IssueRecordingServiceSession
 
     @Before
     fun setup() {
         traceurMessageSender = mock<TraceurMessageSender>()
         underTest =
-            IssueRecordingServiceCommandHandler(
+            IssueRecordingServiceSession(
                 bgExecutor,
                 dialogTransitionAnimator,
                 panelInteractor,
@@ -75,13 +75,13 @@
                 issueRecordingState,
                 iActivityManager,
                 notificationManager,
-                userContextProvider
+                userContextProvider,
             )
     }
 
     @Test
     fun startsTracing_afterReceivingActionStartCommand() {
-        underTest.handleStartCommand()
+        underTest.start()
         bgExecutor.runAllReady()
 
         Truth.assertThat(issueRecordingState.isRecording).isTrue()
@@ -90,7 +90,7 @@
 
     @Test
     fun stopsTracing_afterReceivingStopTracingCommand() {
-        underTest.handleStopCommand(mContext.contentResolver)
+        underTest.stop(mContext.contentResolver)
         bgExecutor.runAllReady()
 
         Truth.assertThat(issueRecordingState.isRecording).isFalse()
@@ -99,7 +99,7 @@
 
     @Test
     fun cancelsNotification_afterReceivingShareCommand() {
-        underTest.handleShareCommand(0, null, mContext)
+        underTest.share(0, null, mContext)
         bgExecutor.runAllReady()
 
         verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>())
@@ -110,7 +110,7 @@
         issueRecordingState.takeBugreport = true
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(iActivityManager).requestBugReportWithExtraAttachment(uri)
@@ -121,7 +121,7 @@
         issueRecordingState.takeBugreport = false
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(traceurMessageSender).shareTraces(mContext, uri)
@@ -131,7 +131,7 @@
     fun closesShade_afterReceivingShareCommand() {
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(panelInteractor).collapsePanels()
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 a0cafcb..c9e958d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -341,7 +341,7 @@
             bouncerActionButton?.onClick?.invoke()
             runCurrent()
 
-            // TODO(b/298026988): Assert that an activity was started once we use ActivityStarter.
+            // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter.
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
new file mode 100644
index 0000000..efde1ec
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.graphics.Region
+import android.view.setSystemGestureExclusionRegion
+import androidx.compose.ui.geometry.Offset
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerGestureFilterTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val displayId = kosmos.displayTracker.defaultDisplayId
+
+    private val underTest = kosmos.sceneContainerGestureFilterFactory.create(displayId)
+    private val activationJob = Job()
+
+    @Test
+    fun shouldFilterGesture_whenNoRegion_returnsFalse() =
+        testScope.runTest {
+            activate()
+            setSystemGestureExclusionRegion(displayId, null)
+            runCurrent()
+
+            assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isFalse()
+        }
+
+    @Test
+    fun shouldFilterGesture_whenOutsideRegion_returnsFalse() =
+        testScope.runTest {
+            activate()
+            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+            runCurrent()
+
+            assertThat(underTest.shouldFilterGesture(Offset(300f, 100f))).isFalse()
+        }
+
+    @Test
+    fun shouldFilterGesture_whenInsideRegion_returnsTrue() =
+        testScope.runTest {
+            activate()
+            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+            runCurrent()
+
+            assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isTrue()
+        }
+
+    @Test(expected = IllegalStateException::class)
+    fun shouldFilterGesture_beforeActivation_throws() =
+        testScope.runTest {
+            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+            runCurrent()
+
+            underTest.shouldFilterGesture(Offset(100f, 100f))
+        }
+
+    @Test(expected = IllegalStateException::class)
+    fun shouldFilterGesture_afterCancellation_throws() =
+        testScope.runTest {
+            activate()
+            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+            runCurrent()
+
+            cancel()
+
+            underTest.shouldFilterGesture(Offset(100f, 100f))
+        }
+
+    private fun TestScope.activate() {
+        underTest.activateIn(testScope, activationJob)
+        runCurrent()
+    }
+
+    private fun TestScope.cancel() {
+        activationJob.cancel()
+        runCurrent()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index a0bb017..e60e742 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -36,10 +36,12 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.fakeOverlaysByKeys
 import com.android.systemui.scene.sceneContainerConfig
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
 import com.android.systemui.scene.shared.logger.sceneLogger
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.settings.displayTracker
 import com.android.systemui.shade.data.repository.fakeShadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
@@ -86,6 +88,8 @@
                 shadeInteractor = kosmos.shadeInteractor,
                 splitEdgeDetector = kosmos.splitEdgeDetector,
                 logger = kosmos.sceneLogger,
+                gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+                displayId = kosmos.displayTracker.defaultDisplayId,
                 motionEventHandlerReceiver = { motionEventHandler ->
                     this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
                 },
@@ -283,10 +287,7 @@
             fakeSceneDataSource.showOverlay(Overlays.NotificationsShade)
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays)
-                .containsExactly(
-                    Overlays.QuickSettingsShade,
-                    Overlays.NotificationsShade,
-                )
+                .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)
 
             val actionableContentKey =
                 underTest.getActionableContentKey(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index d163abf..19ac0cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -287,7 +287,7 @@
 
     @Test
     fun anyExpansion_shadeGreater() =
-        testScope.runTest() {
+        testScope.runTest {
             // WHEN shade is more expanded than QS
             shadeTestUtil.setShadeAndQsExpansion(.5f, 0f)
             runCurrent()
@@ -298,7 +298,7 @@
 
     @Test
     fun anyExpansion_qsGreater() =
-        testScope.runTest() {
+        testScope.runTest {
             // WHEN qs is more expanded than shade
             shadeTestUtil.setShadeAndQsExpansion(0f, .5f)
             runCurrent()
@@ -308,6 +308,36 @@
         }
 
     @Test
+    fun isShadeAnyExpanded_shadeCollapsed() =
+        testScope.runTest {
+            val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+            shadeTestUtil.setShadeExpansion(0f)
+            runCurrent()
+
+            assertThat(isShadeAnyExpanded).isFalse()
+        }
+
+    @Test
+    fun isShadeAnyExpanded_shadePartiallyExpanded() =
+        testScope.runTest {
+            val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+            shadeTestUtil.setShadeExpansion(0.01f)
+            runCurrent()
+
+            assertThat(isShadeAnyExpanded).isTrue()
+        }
+
+    @Test
+    fun isShadeAnyExpanded_shadeFullyExpanded() =
+        testScope.runTest {
+            val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+            shadeTestUtil.setShadeExpansion(1f)
+            runCurrent()
+
+            assertThat(isShadeAnyExpanded).isTrue()
+        }
+
+    @Test
     fun isShadeTouchable_isFalse_whenDeviceAsleepAndNotPulsing() =
         testScope.runTest {
             powerRepository.updateWakefulness(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
index 109cd05..4592b60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -35,6 +35,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -382,4 +383,44 @@
             // THEN user is not interacting
             assertThat(actual).isFalse()
         }
+
+    @Test
+    fun expandNotificationsShade_unsupported() =
+        testScope.runTest {
+            assertThrows(UnsupportedOperationException::class.java) {
+                underTest.expandNotificationsShade("reason")
+            }
+        }
+
+    @Test
+    fun expandQuickSettingsShade_unsupported() =
+        testScope.runTest {
+            assertThrows(UnsupportedOperationException::class.java) {
+                underTest.expandQuickSettingsShade("reason")
+            }
+        }
+
+    @Test
+    fun collapseNotificationsShade_unsupported() =
+        testScope.runTest {
+            assertThrows(UnsupportedOperationException::class.java) {
+                underTest.collapseNotificationsShade("reason")
+            }
+        }
+
+    @Test
+    fun collapseQuickSettingsShade_unsupported() =
+        testScope.runTest {
+            assertThrows(UnsupportedOperationException::class.java) {
+                underTest.collapseQuickSettingsShade("reason")
+            }
+        }
+
+    @Test
+    fun collapseEitherShade_unsupported() =
+        testScope.runTest {
+            assertThrows(UnsupportedOperationException::class.java) {
+                underTest.collapseEitherShade("reason")
+            }
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index f6fe667ff..eb8ea8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -1,12 +1,15 @@
 package com.android.systemui.shade.ui.viewmodel
 
 import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.provider.AlarmClock
 import android.provider.Settings
 import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -18,7 +21,9 @@
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
 import com.android.systemui.testKosmos
@@ -48,7 +53,7 @@
     private val sceneInteractor = kosmos.sceneInteractor
     private val deviceEntryInteractor = kosmos.deviceEntryInteractor
 
-    private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel
+    private val underTest by lazy { kosmos.shadeHeaderViewModel }
 
     @Before
     fun setUp() {
@@ -96,6 +101,7 @@
         }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() =
         testScope.runTest {
             setDeviceEntered(false)
@@ -108,6 +114,25 @@
         }
 
     @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun onSystemIconContainerClicked_lockedOnDualShade_collapsesShadeToLockscreen() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            setDeviceEntered(false)
+            setScene(Scenes.Lockscreen)
+            setOverlay(Overlays.NotificationsShade)
+            assertThat(currentOverlays).isNotEmpty()
+
+            underTest.onSystemIconContainerClicked()
+            runCurrent()
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
         testScope.runTest {
             setDeviceEntered(true)
@@ -119,6 +144,24 @@
             assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
         }
 
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun onSystemIconContainerClicked_unlockedOnDualShade_collapsesShadeToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            setDeviceEntered(true)
+            setScene(Scenes.Gone)
+            setOverlay(Overlays.NotificationsShade)
+            assertThat(currentOverlays).isNotEmpty()
+
+            underTest.onSystemIconContainerClicked()
+            runCurrent()
+
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(currentOverlays).isEmpty()
+        }
+
     companion object {
         private val SUB_1 =
             SubscriptionModel(
@@ -144,6 +187,17 @@
         testScope.runCurrent()
     }
 
+    private fun setOverlay(key: OverlayKey) {
+        val currentOverlays = sceneInteractor.currentOverlays.value + key
+        sceneInteractor.showOverlay(key, "test")
+        sceneInteractor.setTransitionState(
+            MutableStateFlow<ObservableTransitionState>(
+                ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays)
+            )
+        )
+        testScope.runCurrent()
+    }
+
     private fun TestScope.setDeviceEntered(isEntered: Boolean) {
         if (isEntered) {
             // Unlock the device marking the device has entered.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index 7b87aeb..d772e3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -42,7 +42,8 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.util.settings.FakeSettings
@@ -66,7 +67,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+@EnableFlags(NotificationMinimalism.FLAG_NAME)
 class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
 
     private val kosmos =
@@ -76,7 +77,7 @@
                 mock<SysuiStatusBarStateController>().also { mock ->
                     doAnswer { statusBarState.ordinal }.whenever(mock).state
                 }
-            fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+            lockScreenNotificationMinimalismSetting = true
         }
     private val notifPipeline: NotifPipeline = mock()
     private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
@@ -193,7 +194,7 @@
             kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
             assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(child2))
-                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+                .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
             assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
 
@@ -201,7 +202,7 @@
             kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
             assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
             assertThat(promoter.shouldPromoteToTopLevel(child2))
-                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+                .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
             assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
index 2159b86..ea2e25e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -57,7 +57,7 @@
     }
 
     @Test
-    @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+    @EnableFlags(NotificationMinimalism.FLAG_NAME)
     fun topOngoingAndUnseenNotification() = runTest {
         val entry1 = NotificationEntryBuilder().setTag("entry1").build()
         val entry2 = NotificationEntryBuilder().setTag("entry2").build()
@@ -91,4 +91,17 @@
         testScheduler.runCurrent()
         assertThat(settingEnabled).isTrue()
     }
+
+    fun testLockScreenNotificationMinimalismSetting() = runTest {
+        val settingEnabled by
+            collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled())
+
+        kosmos.lockScreenNotificationMinimalismSetting = false
+        testScheduler.runCurrent()
+        assertThat(settingEnabled).isFalse()
+
+        kosmos.lockScreenNotificationMinimalismSetting = true
+        testScheduler.runCurrent()
+        assertThat(settingEnabled).isTrue()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
new file mode 100644
index 0000000..28857a0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -0,0 +1,290 @@
+/*
+ * 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.notification.emptyshade.ui.viewmodel
+
+import android.app.Flags
+import android.app.NotificationManager.Policy
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.data.repository.updateNotificationPolicy
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(ParameterizedAndroidJunit4::class)
+@SmallTest
+@EnableFlags(FooterViewRefactor.FLAG_NAME)
+class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val zenModeRepository = kosmos.zenModeRepository
+    private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+    private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository
+
+    private val underTest = kosmos.emptyShadeViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Test
+    fun areNotificationsHiddenInShade_true() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(hidden).isTrue()
+        }
+
+    @Test
+    fun areNotificationsHiddenInShade_false() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun hasFilteredOutSeenNotifications_true() =
+        testScope.runTest {
+            val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+            runCurrent()
+
+            assertThat(hasFilteredNotifs).isTrue()
+        }
+
+    @Test
+    fun hasFilteredOutSeenNotifications_false() =
+        testScope.runTest {
+            val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+            runCurrent()
+
+            assertThat(hasFilteredNotifs).isFalse()
+        }
+
+    @Test
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    fun text_changesWhenNotifsHiddenInShade() =
+        testScope.runTest {
+            val text by collectLastValue(underTest.text)
+
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            runCurrent()
+
+            assertThat(text).isEqualTo("No notifications")
+
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(text).isEqualTo("Notifications paused by Do Not Disturb")
+        }
+
+    @Test
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    fun text_reflectsModesHidingNotifications() =
+        testScope.runTest {
+            val text by collectLastValue(underTest.text)
+
+            assertThat(text).isEqualTo("No notifications")
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setId("Do not disturb")
+                    .setName("Do not disturb")
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+            assertThat(text).isEqualTo("Notifications paused by Do not disturb")
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setId("Work")
+                    .setName("Work")
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+            assertThat(text).isEqualTo("Notifications paused by Do not disturb and one other mode")
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setId("Gym")
+                    .setName("Gym")
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+            assertThat(text).isEqualTo("Notifications paused by Do not disturb and 2 other modes")
+
+            zenModeRepository.deactivateMode("Do not disturb")
+            zenModeRepository.deactivateMode("Work")
+            runCurrent()
+            assertThat(text).isEqualTo("Notifications paused by Gym")
+        }
+
+    @Test
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    fun footer_isVisibleWhenSeenNotifsAreFilteredOut() =
+        testScope.runTest {
+            val footerVisible by collectLastValue(underTest.footer.isVisible)
+
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+            runCurrent()
+
+            assertThat(footerVisible).isFalse()
+
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+            runCurrent()
+
+            assertThat(footerVisible).isTrue()
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY)
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setId("ID")
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+            assertThat(
+                    onClick?.targetIntent?.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)
+                )
+                .isEqualTo("ID")
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS)
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+
+            assertThat(onClick?.targetIntent?.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 26e1a4d..d12d6f6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -18,12 +18,9 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
-import android.app.NotificationManager.Policy
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
 import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.data.repository.updateNotificationPolicy
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.DisableSceneContainer
@@ -46,7 +43,6 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
-import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.statusbar.policy.fakeConfigurationController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ui.isAnimating
@@ -79,7 +75,6 @@
     private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
     private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
     private val headsUpRepository = kosmos.headsUpNotificationRepository
-    private val zenModeRepository = kosmos.zenModeRepository
 
     private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
 
@@ -266,56 +261,6 @@
         }
 
     @Test
-    fun areNotificationsHiddenInShade_true() =
-        testScope.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            zenModeRepository.updateNotificationPolicy(
-                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
-            )
-            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-            runCurrent()
-
-            assertThat(hidden).isTrue()
-        }
-
-    @Test
-    fun areNotificationsHiddenInShade_false() =
-        testScope.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            zenModeRepository.updateNotificationPolicy(
-                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
-            )
-            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
-            runCurrent()
-
-            assertThat(hidden).isFalse()
-        }
-
-    @Test
-    fun hasFilteredOutSeenNotifications_true() =
-        testScope.runTest {
-            val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
-            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
-            runCurrent()
-
-            assertThat(hasFilteredNotifs).isTrue()
-        }
-
-    @Test
-    fun hasFilteredOutSeenNotifications_false() =
-        testScope.runTest {
-            val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
-            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
-            runCurrent()
-
-            assertThat(hasFilteredNotifs).isFalse()
-        }
-
-    @Test
     fun shouldIncludeFooterView_trueWhenShade() =
         testScope.runTest {
             val shouldIncludeFooterView by collectFooterViewVisibility()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 2c8cc1a..3053672 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,7 +47,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
-import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -248,35 +247,32 @@
 
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_removeWhenReorderingAllowedTrue() {
-        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
-        val hmp = createHeadsUpManagerPhone()
-
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        hmp.showNotification(notifEntry)
-        assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+    fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
         whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
         val hmp = createHeadsUpManagerPhone()
 
         val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
         hmp.showNotification(notifEntry)
-        assertThat(notifEntry.isSeenInShade).isTrue();
+        Assert.assertTrue(notifEntry.isSeenInShade)
     }
 
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+    fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
         whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
         val hmp = createHeadsUpManagerPhone()
 
         val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
         hmp.showNotification(notifEntry)
-        assertThat(notifEntry.isSeenInShade).isFalse();
+        Assert.assertFalse(notifEntry.isSeenInShade)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 0f6dc07..c5ccf9e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -25,6 +25,7 @@
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
 import android.service.notification.SystemZenRules
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.R
@@ -34,6 +35,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
 import com.android.systemui.testKosmos
@@ -379,4 +381,46 @@
 
             assertThat(dndMode!!.isActive).isTrue()
         }
+
+    @Test
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
+        testScope.runTest {
+            val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
+
+            zenModeRepository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("Not active, no list suppression")
+                        .setActive(false)
+                        .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Not active, has list suppression")
+                        .setActive(false)
+                        .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("No list suppression")
+                        .setActive(true)
+                        .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Has list suppression 1")
+                        .setActive(true)
+                        .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Has list suppression 2")
+                        .setActive(true)
+                        .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                        .build(),
+                )
+            )
+            runCurrent()
+
+            assertThat(modesHidingNotifications?.map { it.name })
+                .containsExactly("Has list suppression 1", "Has list suppression 2")
+                .inOrder()
+        }
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index be44dee..73626b4 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -184,7 +184,10 @@
             }
         }
 
-        /** Get the text for secondaryLabel. */
+        /**
+         *  If the current secondaryLabel value is not empty, ignore the given input and return
+         *  the current value. Otherwise return current value.
+         */
         public CharSequence getSecondaryLabel(CharSequence stateText) {
             // Use a local reference as the value might change from other threads
             CharSequence localSecondaryLabel = secondaryLabel;
diff --git a/packages/SystemUI/res/layout/status_bar_no_notifications.xml b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
index 248e611..e26b855 100644
--- a/packages/SystemUI/res/layout/status_bar_no_notifications.xml
+++ b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
@@ -26,6 +26,7 @@
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:gravity="center"
+            android:paddingHorizontal="8dp"
             >
         <TextView
                 android:id="@+id/no_notifications"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e94248d..629c94f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -2047,4 +2047,6 @@
     <!-- SliceView icon size -->
     <dimen name="abc_slice_big_pic_min_height">64dp</dimen>
     <dimen name="abc_slice_big_pic_max_height">64dp</dimen>
+
+    <dimen name="contextual_edu_dialog_bottom_margin">70dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 24b6579..c76b35f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1473,6 +1473,16 @@
     <!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] -->
     <string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string>
 
+    <!-- The text to show in the notifications shade when a mode is suppressing notifications. [CHAR LIMIT=100] -->
+    <string name="modes_suppressing_shade_text">
+        {count, plural, offset:1
+            =0 {No notifications}
+            =1 {Notifications paused by {mode}}
+            =2 {Notifications paused by {mode} and one other mode}
+            other {Notifications paused by {mode} and # other modes}
+        }
+    </string>
+
     <!-- Media projection permission dialog action text. [CHAR LIMIT=60] -->
     <string name="media_projection_action_text">Start now</string>
 
@@ -3808,6 +3818,8 @@
     <!-- Main text of the one line view of a redacted notification -->
     <string name="redacted_notification_single_line_text">Unlock to view</string>
 
+    <!-- Content description for contextual education dialog [CHAR LIMIT=NONE] -->
+    <string name="contextual_education_dialog_title">Contextual education</string>
     <!-- Education notification title for Back [CHAR_LIMIT=100] -->
     <string name="back_edu_notification_title">Use your touchpad to go back</string>
     <!-- Education notification text for Back [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a02c354..b34d6e4 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1720,4 +1720,10 @@
     <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
         <item name="android:windowLightNavigationBar">true</item>
     </style>
+
+    <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+        <!-- To make the dialog wrap to content when the education text is short -->
+        <item name="windowMinWidthMajor">0%</item>
+        <item name="windowMinWidthMinor">0%</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8f55961..0f1da50 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -70,6 +70,7 @@
         "jsr330",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "//frameworks/libs/systemui:msdl",
+        "//frameworks/libs/systemui:view_capture",
     ],
     resource_dirs: [
         "res",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index f358ba2..4db6ab6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.shared.rotation;
 
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
 import android.annotation.DimenRes;
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
@@ -30,7 +33,6 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
@@ -38,6 +40,7 @@
 import androidx.annotation.BoolRes;
 import androidx.core.view.OneShotPreDrawListener;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
 
 /**
@@ -47,7 +50,7 @@
 
     private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
 
-    private final WindowManager mWindowManager;
+    private final ViewCaptureAwareWindowManager mWindowManager;
     private final ViewGroup mKeyButtonContainer;
     private final FloatingRotationButtonView mKeyButtonView;
 
@@ -88,7 +91,8 @@
             @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
             @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
         mContext = context;
-        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+                enableViewCaptureTracing());
         mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
         mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
         mKeyButtonView.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 76df9c9..fb00d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -75,6 +75,9 @@
  * touches are consumed.
  */
 public class TouchMonitor {
+    // An incrementing id used to identify the touch monitor instance.
+    private static int sNextInstanceId = 0;
+
     private final Logger mLogger;
     // This executor is used to protect {@code mActiveTouchSessions} from being modified
     // concurrently. Any operation that adds or removes values should use this executor.
@@ -138,7 +141,7 @@
                     completer.set(predecessor);
                 }
 
-                if (mActiveTouchSessions.isEmpty()) {
+                if (mActiveTouchSessions.isEmpty() && mInitialized) {
                     if (mStopMonitoringPending) {
                         stopMonitoring(false);
                     } else {
@@ -271,7 +274,7 @@
 
         @Override
         public void onDestroy(LifecycleOwner owner) {
-            stopMonitoring(true);
+            destroy();
         }
     };
 
@@ -279,6 +282,11 @@
      * When invoked, instantiates a new {@link InputSession} to monitor touch events.
      */
     private void startMonitoring() {
+        if (!mInitialized) {
+            mLogger.w("attempting to startMonitoring when not initialized");
+            return;
+        }
+
         mLogger.i("startMonitoring(): monitoring started");
         stopMonitoring(true);
 
@@ -587,7 +595,7 @@
         mDisplayHelper = displayHelper;
         mWindowManagerService = windowManagerService;
         mConfigurationInteractor = configurationInteractor;
-        mLoggingName = loggingName + ":TouchMonitor";
+        mLoggingName = loggingName + ":TouchMonitor[" + sNextInstanceId++ + "]";
         mLogger = new Logger(logBuffer, mLoggingName);
     }
 
@@ -613,7 +621,8 @@
      */
     public void destroy() {
         if (!mInitialized) {
-            throw new IllegalStateException("TouchMonitor not initialized");
+            // In the case that we've already been destroyed, this is a no-op
+            return;
         }
 
         stopMonitoring(true);
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
index 8b5a09b..2c026c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
@@ -103,7 +103,7 @@
                 prepareToPerformAction()
                 returnToCall()
             },
-            onLongClick = null
+            onLongClick = null,
         )
     }
 
@@ -115,15 +115,15 @@
                 dozeLogger.logEmergencyCall()
                 startEmergencyDialerActivity()
             },
-            // TODO(b/308001302): The long click detector doesn't work properly, investigate.
+            // TODO(b/369767936): The long click detector doesn't work properly, investigate.
             onLongClick = {
                 if (emergencyAffordanceManager.needsEmergencyAffordance()) {
                     prepareToPerformAction()
 
-                    // TODO(b/298026988): Check that !longPressWasDragged before invoking.
+                    // TODO(b/369767936): Check that !longPressWasDragged before invoking.
                     emergencyAffordanceManager.performEmergencyCall()
                 }
-            }
+            },
         )
     }
 
@@ -143,7 +143,7 @@
             applicationContext.startActivityAsUser(
                 this,
                 ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(),
-                UserHandle(selectedUserInteractor.getSelectedUserId())
+                UserHandle(selectedUserInteractor.getSelectedUserId()),
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index b8c30fe..d6b9211 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -69,7 +69,7 @@
                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
             )
         } else {
-            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR)
+            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR_DISCRETE)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index cbea876..8da4d46 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.dreams.DreamMonitor
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
-import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialCoreStartable
+import com.android.systemui.haptics.msdl.MSDLCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -323,4 +323,9 @@
     @IntoMap
     @ClassKey(BatteryControllerStartable::class)
     abstract fun bindsBatteryControllerStartable(impl: BatteryControllerStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(MSDLCoreStartable::class)
+    abstract fun bindMSDLCoreStartable(impl: MSDLCoreStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 113e001..83f86a7 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,6 +65,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -499,8 +500,11 @@
 
         mDreamOverlayContainerViewController =
                 dreamOverlayComponent.getDreamOverlayContainerViewController();
-        mTouchMonitor = ambientTouchComponent.getTouchMonitor();
-        mTouchMonitor.init();
+
+        if (!SceneContainerFlag.isEnabled()) {
+            mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+            mTouchMonitor.init();
+        }
 
         mStateController.setShouldShowComplications(shouldShowComplications());
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
new file mode 100644
index 0000000..287e85c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 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.education.ui.view
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import android.widget.ToastPresenter
+import com.android.systemui.education.ui.viewmodel.ContextualEduToastViewModel
+import com.android.systemui.res.R
+
+class ContextualEduDialog(context: Context, private val model: ContextualEduToastViewModel) :
+    AlertDialog(context, R.style.ContextualEduDialog) {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setUpWindowProperties()
+        setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.contextual_education_dialog_title))
+        // TODO: b/369791926 - replace the below toast view with a custom dialog view
+        val toastView = ToastPresenter.getTextToastView(context, model.message)
+        setView(toastView)
+        super.onCreate(savedInstanceState)
+    }
+
+    private fun setUpWindowProperties() {
+        window?.apply {
+            setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)
+            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+        }
+        setCanceledOnTouchOutside(false)
+    }
+
+    private fun setWindowPosition() {
+        window?.apply {
+            setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+            this.attributes =
+                WindowManager.LayoutParams().apply {
+                    width = WindowManager.LayoutParams.WRAP_CONTENT
+                    height = WindowManager.LayoutParams.WRAP_CONTENT
+                    copyFrom(attributes)
+                    y =
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.contextual_edu_dialog_bottom_margin
+                        )
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
index e62b26b..913ecdd 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.education.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager
@@ -24,7 +25,6 @@
 import android.content.Intent
 import android.os.Bundle
 import android.os.UserHandle
-import android.widget.Toast
 import androidx.core.app.NotificationCompat
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
@@ -49,7 +49,7 @@
     private val viewModel: ContextualEduViewModel,
     private val context: Context,
     private val notificationManager: NotificationManager,
-    private val createToast: (String) -> Toast
+    private val createDialog: (ContextualEduToastViewModel) -> Dialog,
 ) : CoreStartable {
 
     companion object {
@@ -69,16 +69,23 @@
         viewModel,
         context,
         notificationManager,
-        createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) }
+        createDialog = { model -> ContextualEduDialog(context, model) },
     )
 
+    var dialog: Dialog? = null
+
     override fun start() {
         createEduNotificationChannel()
         applicationScope.launch {
             viewModel.eduContent.collect { contentModel ->
-                when (contentModel) {
-                    is ContextualEduToastViewModel -> showToast(contentModel)
-                    is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                if (contentModel != null) {
+                    when (contentModel) {
+                        is ContextualEduToastViewModel -> showDialog(contentModel)
+                        is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                    }
+                } else {
+                    dialog?.dismiss()
+                    dialog = null
                 }
             }
         }
@@ -95,9 +102,9 @@
         notificationManager.createNotificationChannel(channel)
     }
 
-    private fun showToast(model: ContextualEduToastViewModel) {
-        val toast = createToast(model.message)
-        toast.show()
+    private fun showDialog(model: ContextualEduToastViewModel) {
+        dialog = createDialog(model)
+        dialog?.show()
     }
 
     private fun showNotification(model: ContextualEduNotificationViewModel) {
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
index cd4a8ad..32e7f41 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.education.ui.viewmodel
 
 import android.content.res.Resources
+import android.view.accessibility.AccessibilityManager
 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.contextualeducation.GestureType.HOME
@@ -27,23 +28,63 @@
 import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import javax.inject.Inject
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class ContextualEduViewModel
 @Inject
-constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) {
-    val eduContent: Flow<ContextualEduContentViewModel> =
-        interactor.educationTriggered.filterNotNull().map {
-            if (it.educationUiType == EducationUiType.Notification) {
-                ContextualEduNotificationViewModel(getEduTitle(it), getEduContent(it), it.userId)
-            } else {
-                ContextualEduToastViewModel(getEduContent(it), it.userId)
+constructor(
+    @Main private val resources: Resources,
+    interactor: KeyboardTouchpadEduInteractor,
+    private val accessibilityManagerWrapper: AccessibilityManagerWrapper,
+) {
+
+    companion object {
+        const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500
+    }
+
+    private val timeoutMillis: Long
+        get() =
+            accessibilityManagerWrapper
+                .getRecommendedTimeoutMillis(
+                    DEFAULT_DIALOG_TIMEOUT_MILLIS,
+                    AccessibilityManager.FLAG_CONTENT_TEXT,
+                )
+                .toLong()
+
+    val eduContent: Flow<ContextualEduContentViewModel?> =
+        interactor.educationTriggered
+            .filterNotNull()
+            .map {
+                if (it.educationUiType == EducationUiType.Notification) {
+                    ContextualEduNotificationViewModel(
+                        getEduTitle(it),
+                        getEduContent(it),
+                        it.userId,
+                    )
+                } else {
+                    ContextualEduToastViewModel(getEduContent(it), it.userId)
+                }
+            }
+            .timeout(timeoutMillis, emitAfterTimeout = null)
+
+    private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> {
+        return flatMapLatest {
+            flow {
+                emit(it)
+                delay(timeoutMillis)
+                emit(emitAfterTimeout)
             }
         }
+    }
 
     private fun getEduContent(educationInfo: EducationInfo): String {
         val resourceId =
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0b775ab..47f0ecf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -34,10 +34,14 @@
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.core.StatusBarSimpleFragment
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -55,7 +59,9 @@
         // Internal notification frontend dependencies
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
-        NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+        NotificationMinimalism.token dependsOn NotificationThrottleHun.token
+        ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
+        ModesEmptyShadeFix.token dependsOn modesUi
 
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
@@ -69,6 +75,8 @@
         // Status bar chip dependencies
         statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
         statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
+
+        StatusBarConnectedDisplays.token dependsOn StatusBarSimpleFragment.token
     }
 
     private inline val politeNotifications
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 493afde..aa1873c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -2298,9 +2298,11 @@
                     }
 
                     @Override
-                    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+                    public boolean onScroll(@Nullable MotionEvent e1, MotionEvent e2,
+                            float distanceX,
                             float distanceY) {
                         if (distanceY < 0 && distanceY > distanceX
+                                && e1 != null
                                 && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) {
                             // Downwards scroll from top
                             openShadeAndDismiss();
@@ -2310,9 +2312,11 @@
                     }
 
                     @Override
-                    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                    public boolean onFling(@Nullable MotionEvent e1, MotionEvent e2,
+                            float velocityX,
                             float velocityY) {
                         if (velocityY > 0 && Math.abs(velocityY) > Math.abs(velocityX)
+                                && e1 != null
                                 && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) {
                             // Downwards fling from top
                             openShadeAndDismiss();
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
new file mode 100644
index 0000000..58736c60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.haptics.msdl
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.google.android.msdl.domain.MSDLPlayer
+import com.google.android.msdl.logging.MSDLHistoryLogger
+import java.io.PrintWriter
+import javax.inject.Inject
+
+@SysUISingleton
+class MSDLCoreStartable @Inject constructor(private val msdlPlayer: MSDLPlayer) : CoreStartable {
+    override fun start() {}
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("MSDLPlayer history of the last ${MSDLHistoryLogger.HISTORY_SIZE} events:")
+        msdlPlayer.getHistory().forEach { event -> pw.println("$event") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt
new file mode 100644
index 0000000..144c5ead
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.tutorial
+
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD
+import com.android.systemui.shared.system.SysUiStatsLog
+import javax.inject.Inject
+
+class KeyboardTouchpadTutorialMetricsLogger @Inject constructor() {
+
+    fun logPeripheralTutorialLaunched(entryPointExtra: String?, tutorialTypeExtra: String?) {
+        val entryPoint =
+            when (entryPointExtra) {
+                INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SCHEDULED
+                INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__CONTEXTUAL_EDU
+                else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__APP
+            }
+
+        val tutorialType =
+            when (tutorialTypeExtra) {
+                INTENT_TUTORIAL_TYPE_KEYBOARD ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__KEYBOARD
+                INTENT_TUTORIAL_TYPE_TOUCHPAD ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD
+                else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__BOTH
+            }
+
+        SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType)
+    }
+
+    fun logPeripheralTutorialLaunchedFromSettings() {
+        val entryPoint = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SETTINGS
+        val tutorialType = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD
+        SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
index 5d9dda3..f2afaee 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
@@ -31,6 +31,8 @@
 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG
 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_KEY
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD
@@ -48,7 +50,7 @@
     @Background private val backgroundScope: CoroutineScope,
     @Application private val context: Context,
     private val tutorialSchedulerInteractor: TutorialSchedulerInteractor,
-    private val notificationManager: NotificationManager
+    private val notificationManager: NotificationManager,
 ) {
     fun start() {
         backgroundScope.launch {
@@ -68,7 +70,7 @@
         val extras = Bundle()
         extras.putString(
             Notification.EXTRA_SUBSTITUTE_APP_NAME,
-            context.getString(com.android.internal.R.string.android_system_label)
+            context.getString(com.android.internal.R.string.android_system_label),
         )
 
         val info = getNotificationInfo(tutorialType)!!
@@ -91,7 +93,7 @@
             NotificationChannel(
                 CHANNEL_ID,
                 context.getString(com.android.internal.R.string.android_system_label),
-                NotificationManager.IMPORTANCE_DEFAULT
+                NotificationManager.IMPORTANCE_DEFAULT,
             )
         notificationManager.createNotificationChannel(channel)
     }
@@ -100,13 +102,14 @@
         val intent =
             Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply {
                 putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType)
+                putExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY, INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER)
                 flags = Intent.FLAG_ACTIVITY_NEW_TASK
             }
         return PendingIntent.getActivity(
             context,
             /* requestCode= */ 0,
             intent,
-            PendingIntent.FLAG_IMMUTABLE
+            PendingIntent.FLAG_IMMUTABLE,
         )
     }
 
@@ -118,13 +121,13 @@
                 NotificationInfo(
                     context.getString(R.string.launch_keyboard_tutorial_notification_title),
                     context.getString(R.string.launch_keyboard_tutorial_notification_content),
-                    INTENT_TUTORIAL_TYPE_KEYBOARD
+                    INTENT_TUTORIAL_TYPE_KEYBOARD,
                 )
             TutorialType.TOUCHPAD ->
                 NotificationInfo(
                     context.getString(R.string.launch_touchpad_tutorial_notification_title),
                     context.getString(R.string.launch_touchpad_tutorial_notification_content),
-                    INTENT_TUTORIAL_TYPE_TOUCHPAD
+                    INTENT_TUTORIAL_TYPE_TOUCHPAD,
                 )
             TutorialType.BOTH ->
                 NotificationInfo(
@@ -134,7 +137,7 @@
                     context.getString(
                         R.string.launch_keyboard_touchpad_tutorial_notification_content
                     ),
-                    INTENT_TUTORIAL_TYPE_BOTH
+                    INTENT_TUTORIAL_TYPE_BOTH,
                 )
             TutorialType.NONE -> null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
index c130c6c..29febd3 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
@@ -30,6 +30,7 @@
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
+import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger
 import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen
 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel
@@ -51,6 +52,7 @@
     private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider,
     private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>,
     private val logger: InputDeviceTutorialLogger,
+    private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger,
 ) : ComponentActivity() {
 
     companion object {
@@ -58,6 +60,9 @@
         const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad"
         const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard"
         const val INTENT_TUTORIAL_TYPE_BOTH = "both"
+        const val INTENT_TUTORIAL_ENTRY_POINT_KEY = "entry_point"
+        const val INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER = "scheduler"
+        const val INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU = "contextual_edu"
     }
 
     private val vm by
@@ -86,6 +91,10 @@
             PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) }
         }
         if (savedInstanceState == null) {
+            metricsLogger.logPeripheralTutorialLaunched(
+                intent.getStringExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY),
+                intent.getStringExtra(INTENT_TUTORIAL_TYPE_KEY),
+            )
             logger.logOpenTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL)
         }
     }
@@ -109,7 +118,7 @@
         ACTION_KEY ->
             ActionKeyTutorialScreen(
                 onDoneButtonClicked = vm::onDoneButtonClicked,
-                onBack = vm::onBack
+                onBack = vm::onBack,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index b9a16c4..52263ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -18,6 +18,7 @@
 
 import android.content.ActivityNotFoundException
 import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
 import android.content.res.Configuration
 import android.os.Bundle
 import android.provider.Settings
@@ -125,7 +126,7 @@
     private fun onKeyboardSettingsClicked() {
         try {
             startActivityAsUser(
-                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS),
+                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
                 userTracker.userHandle,
             )
         } catch (e: ActivityNotFoundException) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 68a252b..654c763 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -515,7 +515,13 @@
     }
 
     protected void notifyChange() {
-        mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */));
+        mBgHandler.post(() -> {
+            try {
+                mContentResolver.notifyChange(mSliceUri, null /* observer */);
+            } catch (Exception e) {
+                Log.e(TAG, "Error on mContentResolver.notifyChange()", e);
+            }
+        });
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 416eaba..063adc8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -63,6 +63,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -102,6 +103,7 @@
     private val keyguardClockViewModel: KeyguardClockViewModel,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     private val lockscreenContentViewModelFactory: LockscreenContentViewModel.Factory,
+    private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
     private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
     private val clockInteractor: KeyguardClockInteractor,
     private val keyguardViewMediator: KeyguardViewMediator,
@@ -207,6 +209,7 @@
     private fun createLockscreen(
         context: Context,
         viewModelFactory: LockscreenContentViewModel.Factory,
+        notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
         blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
     ): View {
         val sceneBlueprints =
@@ -222,6 +225,8 @@
                         with(
                             LockscreenContent(
                                 viewModelFactory = viewModelFactory,
+                                notificationScrimViewModelFactory =
+                                    notificationScrimViewModelFactory,
                                 blueprints = sceneBlueprints,
                                 clockInteractor = clockInteractor,
                             )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 0a38ce0..9c7cc81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -147,6 +147,7 @@
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.dagger.KeyguardModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -265,6 +266,7 @@
     private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 17;
     private static final int SYSTEM_READY = 18;
     private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
+    private static final int BOOT_INTERACTOR = 20;
 
     /** Enum for reasons behind updating wakeAndUnlock state. */
     @Retention(RetentionPolicy.SOURCE)
@@ -1390,6 +1392,7 @@
     private final DozeParameters mDozeParameters;
     private final SelectedUserInteractor mSelectedUserInteractor;
     private final KeyguardInteractor mKeyguardInteractor;
+    private final KeyguardTransitionBootInteractor mTransitionBootInteractor;
     @VisibleForTesting
     protected FoldGracePeriodProvider mFoldGracePeriodProvider =
             new FoldGracePeriodProvider();
@@ -1484,6 +1487,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1524,6 +1528,7 @@
         mDozeParameters = dozeParameters;
         mSelectedUserInteractor = selectedUserInteractor;
         mKeyguardInteractor = keyguardInteractor;
+        mTransitionBootInteractor = transitionBootInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -1678,6 +1683,8 @@
             adjustStatusBarLocked();
             mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
 
+            mHandler.obtainMessage(BOOT_INTERACTOR).sendToTarget();
+
             final DreamViewModel dreamViewModel = mDreamViewModel.get();
             final CommunalTransitionViewModel communalViewModel =
                     mCommunalTransitionViewModel.get();
@@ -2705,11 +2712,19 @@
                     message = "SYSTEM_READY";
                     handleSystemReady();
                     break;
+                case BOOT_INTERACTOR:
+                    message = "BOOT_INTERACTOR";
+                    handleBootInteractor();
+                    break;
             }
             Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
         }
     };
 
+    private void handleBootInteractor() {
+        mTransitionBootInteractor.start();
+    }
+
     private void tryKeyguardDone() {
         if (DEBUG) {
             Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - "
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 8a3d017..d0a40ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -59,6 +59,7 @@
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
@@ -175,6 +176,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -225,6 +227,7 @@
                 wmLockscreenVisibilityManager,
                 selectedUserInteractor,
                 keyguardInteractor,
+                transitionBootInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 797a4ec..690ae71 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -23,6 +23,7 @@
 import android.annotation.SuppressLint
 import android.os.Trace
 import android.util.Log
+import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.withContext
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -95,7 +96,7 @@
      * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
      * seed the repository with the appropriate initial state.
      */
-    suspend fun emitInitialStepsFromOff(to: KeyguardState)
+    suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false)
 
     /**
      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
@@ -108,16 +109,14 @@
     suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     )
 }
 
 @SysUISingleton
 class KeyguardTransitionRepositoryImpl
 @Inject
-constructor(
-    @Main val mainDispatcher: CoroutineDispatcher,
-) : KeyguardTransitionRepository {
+constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionRepository {
     /**
      * Each transition between [KeyguardState]s will have an associated Flow. In order to collect
      * these events, clients should call [transition].
@@ -140,7 +139,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.OFF,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -159,12 +158,7 @@
         // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
         // start in.
         emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
     }
 
@@ -217,7 +211,7 @@
                         TransitionStep(
                             info,
                             (animation.animatedValue as Float),
-                            TransitionState.RUNNING
+                            TransitionState.RUNNING,
                         )
                     )
                 }
@@ -266,7 +260,7 @@
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         // There is no fairness guarantee with 'withContext', which means that transitions could
         // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
@@ -282,7 +276,7 @@
     private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         if (updateTransitionId != transitionId) {
             Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
@@ -303,34 +297,51 @@
         lastStep = nextStep
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
-        _currentTransitionInfo.value =
-            TransitionInfo(
-                ownerName = "KeyguardTransitionRepository(boot)",
-                from = KeyguardState.OFF,
-                to = to,
-                animator = null
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
+        val ownerName = "KeyguardTransitionRepository(boot)"
+        // Tests runs on testDispatcher, which is not the main thread, causing the animator thread
+        // check to fail
+        if (testSetup) {
+            _currentTransitionInfo.value =
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator = null,
+                )
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    0f,
+                    TransitionState.STARTED,
+                    ownerName = ownerName,
+                )
             )
 
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                0f,
-                TransitionState.STARTED,
-                ownerName = "KeyguardTransitionRepository(boot)",
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    1f,
+                    TransitionState.FINISHED,
+                    ownerName = ownerName,
+                )
             )
-        )
-
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                1f,
-                TransitionState.FINISHED,
-                ownerName = "KeyguardTransitionRepository(boot)",
-            ),
-        )
+        } else {
+            startTransition(
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator =
+                        ValueAnimator().apply {
+                            interpolator = Interpolators.LINEAR
+                            duration = 933L
+                        },
+                )
+            )
+        }
     }
 
     private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 0343786..840bc0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -106,7 +106,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -127,7 +127,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -147,7 +147,7 @@
                 communalSceneInteractor.changeScene(
                     newScene = CommunalScenes.Communal,
                     loggingReason = "occluded to hub",
-                    transitionKey = CommunalTransitionKeys.SimpleFade
+                    transitionKey = CommunalTransitionKeys.SimpleFade,
                 )
             } else {
                 startTransitionTo(KeyguardState.GLANCEABLE_HUB)
@@ -210,8 +210,9 @@
 
             duration =
                 when (toState) {
-                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.ALTERNATE_BOUNCER -> TO_ALTERNATE_BOUNCER_DURATION
                     KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -220,9 +221,10 @@
     companion object {
         const val TAG = "FromOccludedTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
-        val TO_LOCKSCREEN_DURATION = 933.milliseconds
-        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_ALTERNATE_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_AOD_DURATION = DEFAULT_DURATION
         val TO_DOZING_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_LOCKSCREEN_DURATION = 933.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index ea80911..258232b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -60,12 +60,12 @@
     transitionInteractor: KeyguardTransitionInteractor,
     val dismissInteractor: KeyguardDismissInteractor,
     @Application private val applicationScope: CoroutineScope,
-    sceneInteractor: Lazy<SceneInteractor>,
     deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
     powerInteractor: PowerInteractor,
     alternateBouncerInteractor: AlternateBouncerInteractor,
     shadeInteractor: Lazy<ShadeInteractor>,
     keyguardInteractor: Lazy<KeyguardInteractor>,
+    sceneInteractor: Lazy<SceneInteractor>,
 ) {
     val dismissAction: Flow<DismissAction> = repository.dismissAction
 
@@ -102,20 +102,20 @@
     private val isOnShadeWhileUnlocked: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
             combine(
-                    sceneInteractor.get().currentScene,
+                    shadeInteractor.get().isAnyExpanded,
                     deviceUnlockedInteractor.get().deviceUnlockStatus,
-                ) { scene, unlockStatus ->
-                    unlockStatus.isUnlocked &&
-                        (scene == Scenes.QuickSettings || scene == Scenes.Shade)
+                ) { isAnyExpanded, unlockStatus ->
+                    isAnyExpanded && unlockStatus.isUnlocked
                 }
                 .distinctUntilChanged()
         } else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
             combine(
-                shadeInteractor.get().isAnyExpanded,
-                keyguardInteractor.get().isKeyguardDismissible,
-            ) { isAnyExpanded, keyguardDismissible ->
-                isAnyExpanded && keyguardDismissible
-            }
+                    shadeInteractor.get().isAnyExpanded,
+                    keyguardInteractor.get().isKeyguardDismissible,
+                ) { isAnyExpanded, keyguardDismissible ->
+                    isAnyExpanded && keyguardDismissible
+                }
+                .distinctUntilChanged()
         } else {
             flow {
                 error(
@@ -127,7 +127,20 @@
 
     val executeDismissAction: Flow<() -> KeyguardDone> =
         merge(
-                finishedTransitionToGone,
+                if (SceneContainerFlag.isEnabled) {
+                    // Using currentScene instead of finishedTransitionToGone because of a race
+                    // condition that forms between finishedTransitionToGone and
+                    // isOnShadeWhileUnlocked where the latter emits false before the former emits
+                    // true, causing the merge to not emit until it's too late.
+                    sceneInteractor
+                        .get()
+                        .currentScene
+                        .map { it == Scenes.Gone }
+                        .distinctUntilChanged()
+                        .filter { it }
+                } else {
+                    finishedTransitionToGone
+                },
                 isOnShadeWhileUnlocked.filter { it }.map {},
                 dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction,
             )
@@ -137,10 +150,24 @@
 
     val resetDismissAction: Flow<Unit> =
         combine(
-                transitionInteractor.isFinishedIn(
-                    scene = Scenes.Gone,
-                    stateWithoutSceneContainer = GONE,
-                ),
+                if (SceneContainerFlag.isEnabled) {
+                    // Using currentScene instead of isFinishedIn because of a race condition that
+                    // forms between isFinishedIn(Gone) and isOnShadeWhileUnlocked where the latter
+                    // emits false before the former emits true, causing the evaluation of the
+                    // combine to come up with true, temporarily, before settling on false, which is
+                    // a valid final state. That causes an incorrect reset of the dismiss action to
+                    // occur before it gets executed.
+                    sceneInteractor
+                        .get()
+                        .currentScene
+                        .map { it == Scenes.Gone }
+                        .distinctUntilChanged()
+                } else {
+                    transitionInteractor.isFinishedIn(
+                        scene = Scenes.Gone,
+                        stateWithoutSceneContainer = GONE,
+                    )
+                },
                 transitionInteractor.isFinishedIn(
                     scene = Scenes.Bouncer,
                     stateWithoutSceneContainer = PRIMARY_BOUNCER,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
index b218300..89f636d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.util.Log
-import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -46,7 +45,7 @@
     val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
     val repository: KeyguardTransitionRepository,
-) : CoreStartable {
+) {
 
     /**
      * Whether the lockscreen should be showing when the device starts up for the first time. If not
@@ -60,14 +59,14 @@
         }
     }
 
-    override fun start() {
+    fun start() {
         scope.launch {
             if (internalTransitionInteractor.currentTransitionInfoInternal.value.from != OFF) {
                 Log.e(
                     "KeyguardTransitionInteractor",
                     "showLockscreenOnBoot emitted, but we've already " +
                         "transitioned to a state other than OFF. We'll respect that " +
-                        "transition, but this should not happen."
+                        "transition, but this should not happen.",
                 )
             } else {
                 if (SceneContainerFlag.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index 25b8fd3..b715333 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -27,7 +27,6 @@
 constructor(
     private val interactors: Set<TransitionInteractor>,
     private val auditLogger: KeyguardTransitionAuditLogger,
-    private val bootInteractor: KeyguardTransitionBootInteractor,
     private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor,
     private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor,
 ) : CoreStartable {
@@ -54,7 +53,6 @@
             it.start()
         }
         auditLogger.start()
-        bootInteractor.start()
         statusBarDisableFlagsInteractor.start()
         keyguardStateCallbackInteractor.start()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index f1b9cba..00aa44f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -47,56 +47,53 @@
         constraintLayout.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch("$TAG#viewModel.blueprint") {
-                    viewModel.blueprint
-                        .pairwise(
-                            null as KeyguardBlueprint?,
-                        )
-                        .collect { (prevBlueprint, blueprint) ->
-                            val config = Config.DEFAULT
-                            val transition =
-                                if (
-                                    !KeyguardBottomAreaRefactor.isEnabled &&
-                                        prevBlueprint != null &&
-                                        prevBlueprint != blueprint
-                                ) {
-                                    BaseBlueprintTransition(clockViewModel)
-                                        .addTransition(
-                                            IntraBlueprintTransition(
-                                                config,
-                                                clockViewModel,
-                                                smartspaceViewModel
-                                            )
+                    viewModel.blueprint.pairwise(null as KeyguardBlueprint?).collect {
+                        (prevBlueprint, blueprint) ->
+                        val config = Config.DEFAULT
+                        val transition =
+                            if (
+                                !KeyguardBottomAreaRefactor.isEnabled &&
+                                    prevBlueprint != null &&
+                                    prevBlueprint != blueprint
+                            ) {
+                                BaseBlueprintTransition(clockViewModel)
+                                    .addTransition(
+                                        IntraBlueprintTransition(
+                                            config,
+                                            clockViewModel,
+                                            smartspaceViewModel,
                                         )
-                                } else {
-                                    IntraBlueprintTransition(
-                                        config,
-                                        clockViewModel,
-                                        smartspaceViewModel
                                     )
+                            } else {
+                                IntraBlueprintTransition(
+                                    config,
+                                    clockViewModel,
+                                    smartspaceViewModel,
+                                )
+                            }
+
+                        viewModel.runTransition(constraintLayout, transition, config) {
+                            // Replace sections from the previous blueprint with the new ones
+                            blueprint.replaceViews(
+                                constraintLayout,
+                                prevBlueprint,
+                                config.rebuildSections,
+                            )
+
+                            val cs =
+                                ConstraintSet().apply {
+                                    clone(constraintLayout)
+                                    val emptyLayout = ConstraintSet.Layout()
+                                    knownIds.forEach {
+                                        getConstraint(it).layout.copyFrom(emptyLayout)
+                                    }
+                                    blueprint.applyConstraints(this)
                                 }
 
-                            viewModel.runTransition(constraintLayout, transition, config) {
-                                // Replace sections from the previous blueprint with the new ones
-                                blueprint.replaceViews(
-                                    constraintLayout,
-                                    prevBlueprint,
-                                    config.rebuildSections
-                                )
-
-                                val cs =
-                                    ConstraintSet().apply {
-                                        clone(constraintLayout)
-                                        val emptyLayout = ConstraintSet.Layout()
-                                        knownIds.forEach {
-                                            getConstraint(it).layout.copyFrom(emptyLayout)
-                                        }
-                                        blueprint.applyConstraints(this)
-                                    }
-
-                                logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
-                                cs.applyTo(constraintLayout)
-                            }
+                            logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
+                            cs.applyTo(constraintLayout)
                         }
+                    }
                 }
 
                 launch("$TAG#viewModel.refreshTransition") {
@@ -105,7 +102,8 @@
 
                         viewModel.runTransition(
                             constraintLayout,
-                            IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel),
+                            clockViewModel,
+                            smartspaceViewModel,
                             config,
                         ) {
                             blueprint.rebuildViews(constraintLayout, config.rebuildSections)
@@ -126,7 +124,7 @@
 
     private fun logAlphaVisibilityScaleOfAppliedConstraintSet(
         cs: ConstraintSet,
-        viewModel: KeyguardClockViewModel
+        viewModel: KeyguardClockViewModel,
     ) {
         val currentClock = viewModel.currentClock.value
         if (!DEBUG || currentClock == null) return
@@ -137,19 +135,19 @@
             TAG,
             "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
                 "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha} " +
-                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} "
+                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} ",
         )
         Log.i(
             TAG,
             "applyCsToLargeClock: vis=${cs.getVisibility(largeClockViewId)} " +
                 "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha} " +
                 "scale=${cs.getConstraint(largeClockViewId).transform.scaleX} " +
-                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} "
+                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} ",
         )
         Log.i(
             TAG,
             "applyCsToSmartspaceDate: vis=${cs.getVisibility(smartspaceDateId)} " +
-                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}"
+                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}",
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index aa0a9d9..9a55f7b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -29,18 +29,18 @@
     smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) : TransitionSet() {
 
-    enum class Type(
-        val priority: Int,
-        val animateNotifChanges: Boolean,
-    ) {
+    enum class Type(val priority: Int, val animateNotifChanges: Boolean) {
         ClockSize(100, true),
         ClockCenter(99, false),
         DefaultClockStepping(98, false),
-        SmartspaceVisibility(2, true),
-        DefaultTransition(1, false),
+        SmartspaceVisibility(3, true),
+        DefaultTransition(2, false),
         // When transition between blueprint, we don't need any duration or interpolator but we need
         // all elements go to correct state
-        NoTransition(0, false),
+        NoTransition(1, false),
+        // Similar to NoTransition, except also does not explicitly update any alpha. Used in
+        // OFF->LOCKSCREEN transition
+        Init(0, false),
     }
 
     data class Config(
@@ -57,6 +57,7 @@
     init {
         ordering = ORDERING_TOGETHER
         when (config.type) {
+            Type.Init -> {}
             Type.NoTransition -> {}
             Type.DefaultClockStepping ->
                 addTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index ff84826..a1c963b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -53,14 +53,11 @@
 internal fun ConstraintSet.setVisibility(views: Iterable<View>, visibility: Int) =
     views.forEach { view -> this.setVisibility(view.id, visibility) }
 
-internal fun ConstraintSet.setAlpha(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setAlpha(view.id, alpha) }
+internal fun ConstraintSet.setScaleX(views: Iterable<View>, scaleX: Float) =
+    views.forEach { view -> this.setScaleX(view.id, scaleX) }
 
-internal fun ConstraintSet.setScaleX(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleX(view.id, alpha) }
-
-internal fun ConstraintSet.setScaleY(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleY(view.id, alpha) }
+internal fun ConstraintSet.setScaleY(views: Iterable<View>, scaleY: Float) =
+    views.forEach { view -> this.setScaleY(view.id, scaleY) }
 
 @SysUISingleton
 class ClockSection
@@ -126,8 +123,6 @@
         return constraintSet.apply {
             setVisibility(getTargetClockFace(clock).views, VISIBLE)
             setVisibility(getNonTargetClockFace(clock).views, GONE)
-            setAlpha(getTargetClockFace(clock).views, 1F)
-            setAlpha(getNonTargetClockFace(clock).views, 0F)
             if (!keyguardClockViewModel.isLargeClockVisible.value) {
                 connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
index 3f2ef29..c49e783 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
@@ -28,22 +28,22 @@
 import kotlinx.coroutines.flow.Flow
 
 /**
- * Breaks down ALTERNATE_BOUNCER->GONE transition into discrete steps for corresponding views to
+ * Breaks down ALTERNATE_BOUNCER->OCCLUDED transition into discrete steps for corresponding views to
  * consume.
  */
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class AlternateBouncerToOccludedTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_OCCLUDED_DURATION,
             edge = Edge.create(from = ALTERNATE_BOUNCER, to = OCCLUDED),
         )
 
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index a021de4..ca1a800 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -56,6 +56,7 @@
     occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     primaryBouncerToDozingTransitionViewModel: PrimaryBouncerToDozingTransitionViewModel,
     primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
@@ -67,14 +68,14 @@
                     .map {
                         Utils.getColorAttrDefaultColor(
                             context,
-                            com.android.internal.R.attr.colorSurface
+                            com.android.internal.R.attr.colorSurface,
                         )
                     }
                     .onStart {
                         emit(
                             Utils.getColorAttrDefaultColor(
                                 context,
-                                com.android.internal.R.attr.colorSurface
+                                com.android.internal.R.attr.colorSurface,
                             )
                         )
                     }
@@ -86,23 +87,23 @@
         deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
             if (useBackground) {
                 setOf(
-                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToLockscreenTransitionViewModel
-                            .deviceEntryBackgroundViewAlpha,
+                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         occludedToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        offToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                     )
                     .merge()
                     .onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index 4cf3c4e..1289036 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -24,6 +24,9 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
@@ -37,6 +40,7 @@
 constructor(
     @Main private val handler: Handler,
     private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -49,12 +53,12 @@
     private val transitionListener =
         object : Transition.TransitionListener {
             override fun onTransitionCancel(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
+                if (DEBUG) Log.w(TAG, "onTransitionCancel: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
             override fun onTransitionEnd(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+                if (DEBUG) Log.i(TAG, "onTransitionEnd: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
@@ -86,6 +90,28 @@
 
     fun runTransition(
         constraintLayout: ConstraintLayout,
+        clockViewModel: KeyguardClockViewModel,
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
+        config: Config,
+        apply: () -> Unit,
+    ) {
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
+        runTransition(
+            constraintLayout,
+            IntraBlueprintTransition(newConfig, clockViewModel, smartspaceViewModel),
+            config,
+            apply,
+        )
+    }
+
+    fun runTransition(
+        constraintLayout: ConstraintLayout,
         transition: Transition,
         config: Config,
         apply: () -> Unit,
@@ -103,21 +129,29 @@
             return
         }
 
+        // Don't allow transitions with animations while in OFF state
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
         if (DEBUG) {
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config",
+                    "currentPriority=$currentPriority; config=$newConfig",
             )
         }
 
         // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
         // the running set until the copy is started by the handler.
-        updateTransitions(TransitionData(config)) { add(transition) }
+        updateTransitions(TransitionData(newConfig)) { add(transition) }
         transition.addListener(transitionListener)
 
         handler.post {
-            if (config.terminatePrevious) {
+            if (newConfig.terminatePrevious) {
                 TransitionManager.endTransitions(constraintLayout)
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 10a2e5c..3705c2c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -20,7 +20,6 @@
 import android.graphics.Point
 import android.util.MathUtils
 import android.view.View.VISIBLE
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +34,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
@@ -88,6 +88,8 @@
         AlternateBouncerToGoneTransitionViewModel,
     private val alternateBouncerToLockscreenTransitionViewModel:
         AlternateBouncerToLockscreenTransitionViewModel,
+    private val alternateBouncerToOccludedTransitionViewModel:
+        AlternateBouncerToOccludedTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -112,9 +114,12 @@
     private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
     private val lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel,
+    private val occludedToAlternateBouncerTransitionViewModel:
+        OccludedToAlternateBouncerTransitionViewModel,
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
@@ -201,6 +206,10 @@
             notificationShadeWindowModel.isKeyguardOccluded,
             communalInteractor.isIdleOnCommunal,
             keyguardTransitionInteractor
+                .transitionValue(OFF)
+                .map { it > 1f - offToLockscreenTransitionViewModel.alphaStartAt }
+                .onStart { emit(false) },
+            keyguardTransitionInteractor
                 .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
                 .map { it == 1f }
                 .onStart { emit(false) },
@@ -227,6 +236,7 @@
                         alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        alternateBouncerToOccludedTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
@@ -249,14 +259,16 @@
                         lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
                         lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+                        occludedToAlternateBouncerTransitionViewModel.lockscreenAlpha,
                         occludedToAodTransitionViewModel.lockscreenAlpha,
                         occludedToDozingTransitionViewModel.lockscreenAlpha,
                         occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+                        offToLockscreenTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                     )
-                    .onStart { emit(1f) },
+                    .onStart { emit(0f) },
             ) { hideKeyguard, alpha ->
                 if (hideKeyguard) {
                     0f
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 8d9ccef..88e8968 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -52,18 +52,26 @@
 
     /** Lockscreen views alpha */
     val lockscreenAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1f - it },
-            name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     val shortcutsAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1 - it },
-            onFinish = { 0f },
-            onCancel = { 1f },
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onFinish = { 0f },
+                    onCancel = { 1f },
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..5bfcccb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_ALTERNATE_BOUNCER_DURATION
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down OCCLUDED->ALTERNATE_BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class OccludedToAlternateBouncerTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = TO_ALTERNATE_BOUNCER_DURATION,
+            edge = Edge.create(from = OCCLUDED, to = ALTERNATE_BOUNCER),
+        )
+
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 1eecbd5..b4acce6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -29,23 +30,29 @@
 @SysUISingleton
 class OffToLockscreenTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+
+    private val startTime = 300.milliseconds
+    private val alphaDuration = 633.milliseconds
+    val alphaStartAt = startTime / (alphaDuration + startTime)
 
     private val transitionAnimation =
         animationFlow.setup(
-            duration = 250.milliseconds,
+            duration = startTime + alphaDuration,
             edge = Edge.create(from = OFF, to = LOCKSCREEN),
         )
 
-    val shortcutsAlpha: Flow<Float> =
+    val lockscreenAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
+            startTime = startTime,
+            duration = alphaDuration,
+            interpolator = EMPHASIZED_ACCELERATE,
             onStep = { it },
-            onCancel = { 0f },
         )
 
-    override val deviceEntryParentViewAlpha: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(1f)
+    val shortcutsAlpha: Flow<Float> = lockscreenAlpha
+
+    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 84aae65..222d783 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -111,7 +111,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataManager"
@@ -136,7 +136,7 @@
         active = true,
         resumeAction = null,
         instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID
+        appUid = Process.INVALID_UID,
     )
 
 internal val EMPTY_SMARTSPACE_MEDIA_DATA =
@@ -163,7 +163,7 @@
         Settings.Secure.getInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            1
+            1,
         )
     return Utils.useQsMediaPlayer(context) && flag > 0
 }
@@ -217,7 +217,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -387,7 +387,7 @@
                 uiExecutor,
                 SmartspaceSession.OnTargetsAvailableListener { targets ->
                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                }
+                },
             )
         }
         smartspaceSession?.let { it.requestSmartspaceUpdate() }
@@ -398,12 +398,12 @@
                     if (!allowMediaRecommendations) {
                         dismissSmartspaceRecommendation(
                             key = smartspaceMediaData.targetId,
-                            delay = 0L
+                            delay = 0L,
                         )
                     }
                 }
             },
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
         )
     }
 
@@ -461,7 +461,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaEntries.containsKey(packageName)) {
@@ -497,7 +497,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -509,7 +509,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -609,14 +609,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != currentEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -722,30 +722,32 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     private fun updateState(key: String, state: PlaybackState) {
         mediaEntries.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
-
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+            backgroundExecutor.execute {
+                val token = it.token
+                if (token == null) {
+                    if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                    return@execute
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+                val actions =
+                    createActionsFromState(
+                        it.packageName,
+                        mediaControllerFactory.create(it.token),
+                        UserHandle(it.userId),
+                    )
+
+                // Control buttons
+                // If flag is enabled and controller has a PlaybackState,
+                // create actions from session info
+                // otherwise, no need to update semantic actions.
+                val data =
+                    if (actions != null) {
+                        it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                    } else {
+                        it.copy(isPlaying = isPlayingState(state.state))
+                    }
+                if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                foregroundExecutor.execute { onMediaDataLoaded(key, key, data) }
+            }
         }
     }
 
@@ -773,7 +775,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key = key, userInitiated = userInitiated) },
-            delay
+            delay,
         )
         return existed
     }
@@ -793,12 +795,12 @@
             smartspaceMediaData =
                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                     targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId
+                    instanceId = smartspaceMediaData.instanceId,
                 )
         }
         foregroundExecutor.executeDelayed(
             { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
-            delay
+            delay,
         )
     }
 
@@ -826,7 +828,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -843,7 +845,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -882,7 +884,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -895,7 +897,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -966,7 +968,7 @@
                     appUid = appUid,
                     isExplicit = isExplicit,
                     resumeProgress = progress,
-                )
+                ),
             )
         }
     }
@@ -981,7 +983,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -993,7 +995,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -1057,7 +1059,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1073,7 +1075,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1160,7 +1162,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1169,7 +1171,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaEntries.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1207,7 +1209,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1245,7 +1247,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1282,7 +1284,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1307,7 +1309,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1371,10 +1373,7 @@
                 // There should NOT be more than 1 Smartspace media update. When it happens, it
                 // indicates a bad state or an error. Reset the status accordingly.
                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
-                notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId,
-                    immediately = false,
-                )
+                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
             }
         }
@@ -1420,7 +1419,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1445,7 +1444,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index f2825d0..4f97913 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.domain.pipeline
 
+import android.annotation.WorkerThread
 import android.app.ActivityOptions
 import android.app.BroadcastOptions
 import android.app.Notification
@@ -50,6 +51,7 @@
  * @return a Pair consisting of a list of media actions, and a list of ints representing which of
  *   those actions should be shown in the compact player
  */
+@WorkerThread
 fun createActionsFromState(
     context: Context,
     packageName: String,
@@ -69,7 +71,7 @@
                 context.getString(R.string.controls_media_button_connecting),
                 context.getDrawable(R.drawable.ic_media_connecting_container),
                 // Specify a rebind id to prevent the spinner from restarting on later binds.
-                com.android.internal.R.drawable.progress_small_material
+                com.android.internal.R.drawable.progress_small_material,
             )
         } else if (isPlayingState(state.state)) {
             getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE)
@@ -128,7 +130,7 @@
         nextCustomAction(),
         nextCustomAction(),
         reserveNext,
-        reservePrev
+        reservePrev,
     )
 }
 
@@ -146,7 +148,7 @@
     context: Context,
     controller: MediaController,
     stateActions: Long,
-    @PlaybackState.Actions action: Long
+    @PlaybackState.Actions action: Long,
 ): MediaAction? {
     if (!includesAction(stateActions, action)) {
         return null
@@ -158,7 +160,7 @@
                 context.getDrawable(R.drawable.ic_media_play),
                 { controller.transportControls.play() },
                 context.getString(R.string.controls_media_button_play),
-                context.getDrawable(R.drawable.ic_media_play_container)
+                context.getDrawable(R.drawable.ic_media_play_container),
             )
         }
         PlaybackState.ACTION_PAUSE -> {
@@ -166,7 +168,7 @@
                 context.getDrawable(R.drawable.ic_media_pause),
                 { controller.transportControls.pause() },
                 context.getString(R.string.controls_media_button_pause),
-                context.getDrawable(R.drawable.ic_media_pause_container)
+                context.getDrawable(R.drawable.ic_media_pause_container),
             )
         }
         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
@@ -174,7 +176,7 @@
                 MediaControlDrawables.getPrevIcon(context),
                 { controller.transportControls.skipToPrevious() },
                 context.getString(R.string.controls_media_button_prev),
-                null
+                null,
             )
         }
         PlaybackState.ACTION_SKIP_TO_NEXT -> {
@@ -182,7 +184,7 @@
                 MediaControlDrawables.getNextIcon(context),
                 { controller.transportControls.skipToNext() },
                 context.getString(R.string.controls_media_button_next),
-                null
+                null,
             )
         }
         else -> null
@@ -194,13 +196,13 @@
     context: Context,
     packageName: String,
     controller: MediaController,
-    customAction: PlaybackState.CustomAction
+    customAction: PlaybackState.CustomAction,
 ): MediaAction {
     return MediaAction(
         Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
         { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
         customAction.name,
-        null
+        null,
     )
 }
 
@@ -218,7 +220,7 @@
 /** Generate action buttons based on notification actions */
 fun createActionsFromNotification(
     context: Context,
-    sbn: StatusBarNotification
+    sbn: StatusBarNotification,
 ): Pair<List<MediaNotificationAction>, List<Int>> {
     val notif = sbn.notification
     val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
@@ -229,7 +231,7 @@
     if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
         Log.e(
             TAG,
-            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS"
+            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS",
         )
         actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
     }
@@ -239,7 +241,7 @@
             Log.w(
                 TAG,
                 "Too many notification actions for ${sbn.key}, " +
-                    "limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    "limiting to first $MAX_NOTIFICATION_ACTIONS",
             )
         }
 
@@ -253,7 +255,7 @@
             val themeText =
                 com.android.settingslib.Utils.getColorAttr(
                         context,
-                        com.android.internal.R.attr.textColorPrimary
+                        com.android.internal.R.attr.textColorPrimary,
                     )
                     .defaultColor
 
@@ -271,7 +273,7 @@
                     action.isAuthenticationRequired,
                     action.actionIntent,
                     mediaActionIcon,
-                    action.title
+                    action.title,
                 )
             actionIcons.add(mediaAction)
         }
@@ -288,7 +290,7 @@
  */
 fun getNotificationActions(
     actions: List<MediaNotificationAction>,
-    activityStarter: ActivityStarter
+    activityStarter: ActivityStarter,
 ): List<MediaAction> {
     return actions.map { action ->
         val runnable =
@@ -303,7 +305,7 @@
                             activityStarter.dismissKeyguardThenExecute(
                                 { sendPendingIntent(action.actionIntent) },
                                 {},
-                                true
+                                true,
                             )
                         else -> sendPendingIntent(actionIntent)
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 5f0a9f8..fd7b6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -119,7 +119,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataProcessor"
@@ -177,7 +177,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -365,7 +365,7 @@
                 secureSettings.getBoolForUser(
                     Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
                     true,
-                    UserHandle.USER_CURRENT
+                    UserHandle.USER_CURRENT,
                 )
 
             useQsMediaPlayer && flag
@@ -386,7 +386,7 @@
                 if (!allowMediaRecommendations) {
                     dismissSmartspaceRecommendation(
                         key = mediaDataRepository.smartspaceMediaData.value.targetId,
-                        delay = 0L
+                        delay = 0L,
                     )
                 }
             }
@@ -413,7 +413,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) {
@@ -449,7 +449,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -461,7 +461,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -582,30 +582,37 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     internal fun updateState(key: String, state: PlaybackState) {
         mediaDataRepository.mediaEntries.value.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
+            applicationScope.launch {
+                withContext(backgroundDispatcher) {
+                    val token = it.token
+                    if (token == null) {
+                        if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                        return@withContext
+                    }
+                    val actions =
+                        createActionsFromState(
+                            it.packageName,
+                            mediaControllerFactory.create(it.token),
+                            UserHandle(it.userId),
+                        )
 
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+                    // Control buttons
+                    // If flag is enabled and controller has a PlaybackState,
+                    // create actions from session info
+                    // otherwise, no need to update semantic actions.
+                    val data =
+                        if (actions != null) {
+                            it.copy(
+                                semanticActions = actions,
+                                isPlaying = isPlayingState(state.state),
+                            )
+                        } else {
+                            it.copy(isPlaying = isPlayingState(state.state))
+                        }
+                    if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                    withContext(mainDispatcher) { onMediaDataLoaded(key, key, data) }
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+            }
         }
     }
 
@@ -633,7 +640,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key, userInitiated = userInitiated) },
-            delayMs
+            delayMs,
         )
         return existed
     }
@@ -657,7 +664,7 @@
         if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
             foregroundExecutor.executeDelayed(
                 { notifySmartspaceMediaDataRemoved(key, immediately = true) },
-                delay
+                delay,
             )
         }
     }
@@ -677,7 +684,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -694,7 +701,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -733,7 +740,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -746,7 +753,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -818,7 +825,7 @@
                     isExplicit = isExplicit,
                     resumeProgress = progress,
                     smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
-                )
+                ),
             )
         }
     }
@@ -887,14 +894,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != oldEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -911,7 +918,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -923,7 +930,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -987,7 +994,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1003,7 +1010,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1093,7 +1100,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1102,7 +1109,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaDataRepository.mediaEntries.value.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1151,7 +1158,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1189,7 +1196,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1226,7 +1233,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1251,7 +1258,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1291,7 +1298,7 @@
                 } else {
                     notifySmartspaceMediaDataRemoved(
                         smartspaceMediaData.targetId,
-                        immediately = false
+                        immediately = false,
                     )
                     mediaDataRepository.setRecommendation(
                         SmartspaceMediaData(
@@ -1362,7 +1369,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1387,7 +1394,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
@@ -1513,7 +1520,7 @@
             data: MediaData,
             immediately: Boolean = true,
             receivedSmartspaceCardLatency: Int = 0,
-            isSsReactivated: Boolean = false
+            isSsReactivated: Boolean = false,
         ) {}
 
         /**
@@ -1526,7 +1533,7 @@
         fun onSmartspaceMediaDataLoaded(
             key: String,
             data: SmartspaceMediaData,
-            shouldPrioritize: Boolean = false
+            shouldPrioritize: Boolean = false,
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 275f1ee..39cedc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -128,7 +128,7 @@
         data: MediaData,
         immediately: Boolean,
         receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
+        isSsReactivated: Boolean,
     ) {
         var reusedListener: PlaybackStateListener? = null
 
@@ -183,7 +183,7 @@
     override fun onSmartspaceMediaDataLoaded(
         key: String,
         data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
+        shouldPrioritize: Boolean,
     ) {
         if (!mediaFlags.isPersistentSsCardEnabled()) return
 
@@ -259,7 +259,9 @@
         }
 
         override fun onPlaybackStateChanged(state: PlaybackState?) {
-            processState(state, dispatchEvents = true, currentResumption = resumption)
+            bgExecutor.execute {
+                processState(state, dispatchEvents = true, currentResumption = resumption)
+            }
         }
 
         override fun onSessionDestroyed() {
@@ -276,6 +278,7 @@
             }
         }
 
+        @WorkerThread
         private fun processState(
             state: PlaybackState?,
             dispatchEvents: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index a0fb0bf2..72650ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.annotation.WorkerThread
 import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
@@ -41,6 +42,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -137,7 +139,7 @@
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
     @Main private val mainDispatcher: CoroutineDispatcher,
-    @Main executor: DelayableExecutor,
+    @Main private val uiExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val mediaManager: MediaDataManager,
@@ -227,7 +229,7 @@
     private var carouselLocale: Locale? = null
 
     private val animationScaleObserver: ContentObserver =
-        object : ContentObserver(executor, 0) {
+        object : ContentObserver(uiExecutor, 0) {
             override fun onChange(selfChange: Boolean) {
                 if (!SceneContainerFlag.isEnabled) {
                     MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
@@ -350,7 +352,7 @@
             MediaCarouselScrollHandler(
                 mediaCarousel,
                 pageIndicator,
-                executor,
+                uiExecutor,
                 this::onSwipeToDismiss,
                 this::updatePageIndicatorLocation,
                 this::updateSeekbarListening,
@@ -458,7 +460,17 @@
                     isSsReactivated: Boolean,
                 ) {
                     debugLogger.logMediaLoaded(key, data.active)
-                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
+                    val onUiExecutionEnd =
+                        if (mediaControlsUmoInflationInBackground()) {
+                            Runnable {
+                                if (immediately) {
+                                    updateHostVisibility()
+                                }
+                            }
+                        } else {
+                            null
+                        }
+                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) {
                         // Log card received if a new resumable media card is added
                         MediaPlayerData.getMediaPlayer(key)?.let {
                             logSmartspaceCardReported(
@@ -980,6 +992,7 @@
         oldKey: String?,
         data: MediaData,
         isSsReactivated: Boolean,
+        onUiExecutionEnd: Runnable? = null,
     ): Boolean =
         traceSection("MediaCarouselController#addOrUpdatePlayer") {
             MediaPlayerData.moveIfExists(oldKey, key)
@@ -987,76 +1000,119 @@
             val curVisibleMediaKey =
                 MediaPlayerData.visiblePlayerKeys()
                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-            if (existingPlayer == null) {
-                val newPlayer = mediaControlPanelFactory.get()
-                if (SceneContainerFlag.isEnabled) {
-                    newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx
-                    newPlayer.mediaViewController.heightInSceneContainerPx =
-                        heightInSceneContainerPx
-                }
-                newPlayer.attachPlayer(
-                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
-                )
-                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-                val lp =
-                    LinearLayout.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                    )
-                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-                newPlayer.bindPlayer(data, key)
-                newPlayer.setListening(
-                    mediaCarouselScrollHandler.visibleToUser && currentlyExpanded
-                )
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    newPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
-                // Media data added from a recommendation card should starts playing.
-                if (
-                    (shouldScrollToKey && data.isPlaying == true) ||
-                        (!shouldScrollToKey && data.active)
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+            if (mediaControlsUmoInflationInBackground()) {
+                if (existingPlayer == null) {
+                    bgExecutor.execute {
+                        val mediaViewHolder = createMediaViewHolderInBg()
+                        // Add the new player in the main thread.
+                        uiExecutor.execute {
+                            setupNewPlayer(
+                                key,
+                                data,
+                                isSsReactivated,
+                                curVisibleMediaKey,
+                                mediaViewHolder,
+                            )
+                            updatePageIndicator()
+                            mediaCarouselScrollHandler.onPlayersChanged()
+                            mediaFrame.requiresRemeasuring = true
+                            onUiExecutionEnd?.run()
+                        }
+                    }
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
+                    updatePageIndicator()
+                    mediaCarouselScrollHandler.onPlayersChanged()
+                    mediaFrame.requiresRemeasuring = true
+                    onUiExecutionEnd?.run()
                 }
             } else {
-                existingPlayer.bindPlayer(data, key)
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    existingPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
-                // In case of recommendations hits.
-                // Check the playing status of media player and the package name.
-                // To make sure we scroll to the right app's media player.
-                if (
-                    isReorderingAllowed ||
-                        shouldScrollToKey &&
-                            data.isPlaying == true &&
-                            packageName == data.packageName
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+                if (existingPlayer == null) {
+                    val mediaViewHolder =
+                        MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+                    setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder)
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
                 }
+                updatePageIndicator()
+                mediaCarouselScrollHandler.onPlayersChanged()
+                mediaFrame.requiresRemeasuring = true
+                onUiExecutionEnd?.run()
             }
-            updatePageIndicator()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            mediaFrame.requiresRemeasuring = true
             return existingPlayer == null
         }
 
+    private fun updatePlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        existingPlayer: MediaControlPanel,
+    ) {
+        existingPlayer.bindPlayer(data, key)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            existingPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
+        // In case of recommendations hits.
+        // Check the playing status of media player and the package name.
+        // To make sure we scroll to the right app's media player.
+        if (
+            isReorderingAllowed ||
+                shouldScrollToKey && data.isPlaying == true && packageName == data.packageName
+        ) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    private fun setupNewPlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        mediaViewHolder: MediaViewHolder,
+    ) {
+        val newPlayer = mediaControlPanelFactory.get()
+        newPlayer.attachPlayer(mediaViewHolder)
+        newPlayer.mediaViewController.sizeChangedListener =
+            this@MediaCarouselController::updateCarouselDimensions
+        val lp =
+            LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+            )
+        newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+        newPlayer.bindPlayer(data, key)
+        newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            newPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
+        // Media data added from a recommendation card should starts playing.
+        if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    @WorkerThread
+    private fun createMediaViewHolderInBg(): MediaViewHolder {
+        return MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+    }
+
     private fun addSmartspaceMediaRecommendations(
         key: String,
         data: SmartspaceMediaData,
@@ -1173,8 +1229,16 @@
         val previousVisibleKey =
             MediaPlayerData.visiblePlayerKeys()
                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+        val onUiExecutionEnd = Runnable {
+            if (recreateMedia) {
+                reorderAllPlayers(previousVisibleKey)
+            }
+        }
 
-        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
+        val mediaDataList = MediaPlayerData.mediaData()
+        // Do not loop through the original list of media data because the re-addition of media data
+        // is being executed in background thread.
+        mediaDataList.forEach { (key, data, isSsMediaRec) ->
             if (isSsMediaRec) {
                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
@@ -1185,6 +1249,7 @@
                         MediaPlayerData.shouldPrioritizeSs,
                     )
                 }
+                onUiExecutionEnd.run()
             } else {
                 val isSsReactivated = MediaPlayerData.isSsReactivated(key)
                 if (recreateMedia) {
@@ -1195,11 +1260,9 @@
                     oldKey = null,
                     data = data,
                     isSsReactivated = isSsReactivated,
+                    onUiExecutionEnd = onUiExecutionEnd,
                 )
             }
-            if (recreateMedia) {
-                reorderAllPlayers(previousVisibleKey)
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
index 8660d12..782da4b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.ui.controller
 
 import com.android.app.tracing.traceSection
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.util.animation.MeasurementOutput
@@ -71,23 +72,34 @@
      */
     fun updateCarouselDimensions(
         @MediaLocation location: Int,
-        hostState: MediaHostState
+        hostState: MediaHostState,
     ): MeasurementOutput =
         traceSection("MediaHostStatesManager#updateCarouselDimensions") {
             val result = MeasurementOutput(0, 0)
+            var changed = false
             for (controller in controllers) {
                 val measurement = controller.getMeasurementsForState(hostState)
                 measurement?.let {
                     if (it.measuredHeight > result.measuredHeight) {
                         result.measuredHeight = it.measuredHeight
+                        changed = true
                     }
                     if (it.measuredWidth > result.measuredWidth) {
                         result.measuredWidth = it.measuredWidth
+                        changed = true
                     }
                 }
             }
-            carouselSizes[location] = result
-            return result
+            if (mediaControlsUmoInflationInBackground()) {
+                // Set carousel size if result measurements changed. This avoids setting carousel
+                // size when this method gets called before the addition of media view controllers
+                if (!carouselSizes.contains(location) || changed) {
+                    carouselSizes[location] = result
+                }
+            } else {
+                carouselSizes[location] = result
+            }
+            return carouselSizes[location] ?: result
         }
 
     /** Add a callback to be called when a MediaState has updated */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
index 09a6181..5ddc347 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
@@ -20,6 +20,7 @@
 import android.util.ArraySet
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -91,8 +92,10 @@
                 data: MediaData,
                 immediately: Boolean,
                 receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
+                isSsReactivated: Boolean,
             ) {
+                if (mediaControlsUmoInflationInBackground()) return
+
                 if (immediately) {
                     updateViewVisibility()
                 }
@@ -101,7 +104,7 @@
             override fun onSmartspaceMediaDataLoaded(
                 key: String,
                 data: SmartspaceMediaData,
-                shouldPrioritize: Boolean
+                shouldPrioritize: Boolean,
             ) {
                 updateViewVisibility()
             }
@@ -171,7 +174,7 @@
                         input.widthMeasureSpec =
                             View.MeasureSpec.makeMeasureSpec(
                                 View.MeasureSpec.getSize(input.widthMeasureSpec),
-                                View.MeasureSpec.EXACTLY
+                                View.MeasureSpec.EXACTLY,
                             )
                     }
                     // This will trigger a state change that ensures that we now have a state
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 228b576..d413474 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -54,6 +54,7 @@
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
 import com.android.systemui.res.R
+import com.android.systemui.shared.system.ActivityManagerWrapper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.AsyncActivityLauncher
 import java.lang.IllegalArgumentException
@@ -62,9 +63,10 @@
 class MediaProjectionAppSelectorActivity(
     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
     private val activityLauncher: AsyncActivityLauncher,
+    private val activityManager: ActivityManagerWrapper,
     /** This is used to override the dependency in a screenshot test */
     @VisibleForTesting
-    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
+    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?,
 ) :
     ChooserActivity(),
     MediaProjectionAppSelectorView,
@@ -74,8 +76,9 @@
     @Inject
     constructor(
         componentFactory: MediaProjectionAppSelectorComponent.Factory,
-        activityLauncher: AsyncActivityLauncher
-    ) : this(componentFactory, activityLauncher, listControllerFactory = null)
+        activityLauncher: AsyncActivityLauncher,
+        activityManager: ActivityManagerWrapper,
+    ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null)
 
     private val lifecycleRegistry = LifecycleRegistry(this)
     override val lifecycle = lifecycleRegistry
@@ -100,7 +103,7 @@
                 callingPackage = callingPackage,
                 view = this,
                 resultHandler = this,
-                isFirstStart = savedInstanceState == null
+                isFirstStart = savedInstanceState == null,
             )
         component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
 
@@ -113,7 +116,7 @@
         intent.configureChooserIntent(
             resources,
             component.hostUserHandle,
-            component.personalProfileUserHandle
+            component.personalProfileUserHandle,
         )
 
         reviewGrantedConsentRequired =
@@ -180,7 +183,13 @@
         // is created and ready to be captured.
         val activityStarted =
             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
-                returnSelectedApp(launchCookie, taskId = -1)
+                if (targetInfo.resolvedComponentName == callingActivity) {
+                    // If attempting to launch the app used to launch the MediaProjection, then
+                    // provide the task id since the launch cookie won't match the existing task
+                    returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId)
+                } else {
+                    returnSelectedApp(launchCookie, taskId = -1)
+                }
             }
 
         // Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -213,7 +222,7 @@
             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                 RECORD_CANCEL,
                 reviewGrantedConsentRequired,
-                /* projection= */ null
+                /* projection= */ null,
             )
             if (isFinishing) {
                 // Only log dismissed when actually finishing, and not when changing configuration.
@@ -246,7 +255,7 @@
             val resultReceiver =
                 intent.getParcelableExtra(
                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
-                    ResultReceiver::class.java
+                    ResultReceiver::class.java,
                 ) as ResultReceiver
             val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId)
             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
@@ -260,8 +269,8 @@
             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
 
-            projection.setLaunchCookie(launchCookie)
-            projection.setTaskId(taskId)
+            projection.launchCookie = launchCookie
+            projection.taskId = taskId
 
             val intent = Intent()
             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
@@ -270,7 +279,7 @@
             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                 RECORD_CONTENT_TASK,
                 reviewGrantedConsentRequired,
-                projection
+                projection,
             )
         }
 
@@ -457,7 +466,7 @@
      */
     private class RecyclerViewExpandingAccessibilityDelegate(
         rdl: ResolverDrawerLayout,
-        view: RecyclerView
+        view: RecyclerView,
     ) : RecyclerViewAccessibilityDelegate(view) {
 
         private val delegate = AppListAccessibilityDelegate(rdl)
@@ -465,7 +474,7 @@
         override fun onRequestSendAccessibilityEvent(
             host: ViewGroup,
             child: View,
-            event: AccessibilityEvent
+            event: AccessibilityEvent,
         ): Boolean {
             super.onRequestSendAccessibilityEvent(host, child, event)
             return delegate.onRequestSendAccessibilityEvent(host, child, event)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 8351597..c3729c0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -68,12 +68,12 @@
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
+import dagger.Lazy;
+
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 public class MediaProjectionPermissionActivity extends Activity {
     private static final String TAG = "MediaProjectionPermissionActivity";
     private static final float MAX_APP_NAME_SIZE_PX = 500f;
@@ -132,8 +132,7 @@
                 mPackageName = launchingIntent.getStringExtra(
                         EXTRA_PACKAGE_REUSING_GRANTED_CONSENT);
             } else {
-                setResult(RESULT_CANCELED);
-                finish(RECORD_CANCEL, /* projection= */ null);
+                finishAsCancelled();
                 return;
             }
         }
@@ -145,8 +144,7 @@
             mUid = aInfo.uid;
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Unable to look up package name", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
             return;
         }
 
@@ -176,15 +174,13 @@
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error checking projection permissions", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
             return;
         }
 
         if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
             if (showScreenCaptureDisabledDialogIfNeeded()) {
-                setResult(RESULT_CANCELED);
-                finish(RECORD_CANCEL, /* projection= */ null);
+                finishAsCancelled();
                 return;
             }
         }
@@ -346,6 +342,21 @@
     private void requestDeviceUnlock() {
         mKeyguardManager.requestDismissKeyguard(this,
                 new KeyguardManager.KeyguardDismissCallback() {
+
+                    @Override
+                    public void onDismissError() {
+                        if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) {
+                            finishAsCancelled();
+                        }
+                    }
+
+                    @Override
+                    public void onDismissCancelled() {
+                        if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) {
+                            finishAsCancelled();
+                        }
+                    }
+
                     @Override
                     public void onDismissSucceeded() {
                         mDialog.show();
@@ -386,8 +397,7 @@
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error granting projection permission", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
         } finally {
             if (mDialog != null) {
                 mDialog.dismiss();
@@ -436,6 +446,14 @@
         }
     }
 
+    /**
+     * Finishes this activity and cancel the projection request.
+     */
+    private void finishAsCancelled() {
+        setResult(RESULT_CANCELED);
+        finish(RECORD_CANCEL, /* projection= */ null);
+    }
+
     @Nullable
     private MediaProjectionConfig getMediaProjectionConfig() {
         Intent intent = getIntent();
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index 5be225c..0e54041 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -16,12 +16,19 @@
 
 package com.android.systemui.notifications.ui.viewmodel
 
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 /**
  * Models UI state used to render the content of the notifications shade overlay.
@@ -34,13 +41,40 @@
 constructor(
     val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
     val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
-    private val sceneInteractor: SceneInteractor,
-) {
+    val sceneInteractor: SceneInteractor,
+    private val shadeInteractor: ShadeInteractor,
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                sceneInteractor.currentScene.collect { currentScene ->
+                    when (currentScene) {
+                        // TODO(b/369513770): The ShadeSession should be preserved in this scenario.
+                        Scenes.Bouncer ->
+                            shadeInteractor.collapseNotificationsShade(
+                                loggingReason = "bouncer shown while shade is open"
+                            )
+                    }
+                }
+            }
+
+            launch {
+                shadeInteractor.isShadeTouchable
+                    .distinctUntilChanged()
+                    .filter { !it }
+                    .collect {
+                        shadeInteractor.collapseNotificationsShade(
+                            loggingReason = "device became non-interactive"
+                        )
+                    }
+            }
+        }
+        awaitCancellation()
+    }
+
     fun onScrimClicked() {
-        sceneInteractor.hideOverlay(
-            overlay = Overlays.NotificationsShade,
-            loggingReason = "Shade scrim clicked",
-        )
+        shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked")
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index c807960..ba0d938 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -17,12 +17,15 @@
 package com.android.systemui.qs.composefragment
 
 import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Bundle
 import android.util.IndentingPrintWriter
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
@@ -185,62 +188,76 @@
         savedInstanceState: Bundle?,
     ): View {
         val context = inflater.context
-        return ComposeView(context).apply {
-            setBackPressedDispatcher()
-            setContent {
-                PlatformTheme {
-                    val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
+        val composeView =
+            ComposeView(context).apply {
+                setBackPressedDispatcher()
+                setContent {
+                    PlatformTheme {
+                        val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
 
-                    AnimatedVisibility(
-                        visible = visible,
-                        modifier =
-                            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
-                                .thenIf(notificationScrimClippingParams.isEnabled) {
-                                    Modifier.notificationScrimClip(
-                                        notificationScrimClippingParams.leftInset,
-                                        notificationScrimClippingParams.top,
-                                        notificationScrimClippingParams.rightInset,
-                                        notificationScrimClippingParams.bottom,
-                                        notificationScrimClippingParams.radius,
+                        AnimatedVisibility(
+                            visible = visible,
+                            modifier =
+                                Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+                                    .thenIf(notificationScrimClippingParams.isEnabled) {
+                                        Modifier.notificationScrimClip(
+                                            notificationScrimClippingParams.leftInset,
+                                            notificationScrimClippingParams.top,
+                                            notificationScrimClippingParams.rightInset,
+                                            notificationScrimClippingParams.bottom,
+                                            notificationScrimClippingParams.radius,
+                                        )
+                                    }
+                                    .graphicsLayer { elevation = 4.dp.toPx() },
+                        ) {
+                            val isEditing by
+                                viewModel.containerViewModel.editModeViewModel.isEditing
+                                    .collectAsStateWithLifecycle()
+                            val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
+                            AnimatedContent(
+                                targetState = isEditing,
+                                transitionSpec = {
+                                    fadeIn(animationSpecEditMode) togetherWith
+                                        fadeOut(animationSpecEditMode)
+                                },
+                                label = "EditModeAnimatedContent",
+                            ) { editing ->
+                                if (editing) {
+                                    val qqsPadding by
+                                        viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+                                    EditMode(
+                                        viewModel = viewModel.containerViewModel.editModeViewModel,
+                                        modifier =
+                                            Modifier.fillMaxWidth()
+                                                .padding(top = { qqsPadding })
+                                                .padding(
+                                                    horizontal = {
+                                                        QuickSettingsShade.Dimensions.Padding
+                                                            .roundToPx()
+                                                    }
+                                                ),
                                     )
+                                } else {
+                                    CollapsableQuickSettingsSTL()
                                 }
-                                .graphicsLayer { elevation = 4.dp.toPx() },
-                    ) {
-                        val isEditing by
-                            viewModel.containerViewModel.editModeViewModel.isEditing
-                                .collectAsStateWithLifecycle()
-                        val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
-                        AnimatedContent(
-                            targetState = isEditing,
-                            transitionSpec = {
-                                fadeIn(animationSpecEditMode) togetherWith
-                                    fadeOut(animationSpecEditMode)
-                            },
-                            label = "EditModeAnimatedContent",
-                        ) { editing ->
-                            if (editing) {
-                                val qqsPadding by
-                                    viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
-                                EditMode(
-                                    viewModel = viewModel.containerViewModel.editModeViewModel,
-                                    modifier =
-                                        Modifier.fillMaxWidth()
-                                            .padding(top = { qqsPadding })
-                                            .padding(
-                                                horizontal = {
-                                                    QuickSettingsShade.Dimensions.Padding
-                                                        .roundToPx()
-                                                }
-                                            ),
-                                )
-                            } else {
-                                CollapsableQuickSettingsSTL()
                             }
                         }
                     }
                 }
             }
-        }
+
+        val frame =
+            FrameLayoutTouchPassthrough(
+                context,
+                { notificationScrimClippingParams.isEnabled },
+                { notificationScrimClippingParams.top },
+            )
+        frame.addView(
+            composeView,
+            FrameLayout.LayoutParams.MATCH_PARENT,
+            FrameLayout.LayoutParams.MATCH_PARENT,
+        )
+        return frame
     }
 
     /**
@@ -762,3 +779,26 @@
 }
 
 private const val EDIT_MODE_TIME_MILLIS = 500
+
+/**
+ * Ignore touches below the value returned by [clippingTopProvider], when clipping is enabled, as
+ * per [clippingEnabledProvider].
+ */
+private class FrameLayoutTouchPassthrough(
+    context: Context,
+    private val clippingEnabledProvider: () -> Boolean,
+    private val clippingTopProvider: () -> Int,
+) : FrameLayout(context) {
+    override fun isTransformedTouchPointInView(
+        x: Float,
+        y: Float,
+        child: View?,
+        outLocalPoint: PointF?,
+    ): Boolean {
+        return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) {
+            false
+        } else {
+            super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
+        }
+    }
+}
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 278352c..ead38f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.log.dagger.QSConfigLog
 import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.QSTile.State
 import com.android.systemui.statusbar.StatusBarState
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -57,6 +58,7 @@
     fun d(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
     }
+
     fun i(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
     }
@@ -73,7 +75,19 @@
                 str1 = tileSpec
                 str2 = reason
             },
-            { "[$str1] Tile destroyed. Reason: $str2" }
+            { "[$str1] Tile destroyed. Reason: $str2" },
+        )
+    }
+
+    fun logStateChanged(tileSpec: String, state: State) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                str2 = state.toString()
+            },
+            { "[$str1] Tile state=$str2" },
         )
     }
 
@@ -85,7 +99,7 @@
                 bool1 = listening
                 str1 = tileSpec
             },
-            { "[$str1] Tile listening=$bool1" }
+            { "[$str1] Tile listening=$bool1" },
         )
     }
 
@@ -98,7 +112,7 @@
                 str1 = containerName
                 str2 = allSpecs
             },
-            { "Tiles listening=$bool1 in $str1. $str2" }
+            { "Tiles listening=$bool1 in $str1. $str2" },
         )
     }
 
@@ -112,7 +126,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -124,7 +138,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling click." }
+            { "[$str1][$int1] Tile handling click." },
         )
     }
 
@@ -138,7 +152,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -150,7 +164,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling secondary click." }
+            { "[$str1][$int1] Tile handling secondary click." },
         )
     }
 
@@ -164,7 +178,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -176,7 +190,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling long click." }
+            { "[$str1][$int1] Tile handling long click." },
         )
     }
 
@@ -189,7 +203,7 @@
                 int1 = lastType
                 str2 = callback
             },
-            { "[$str1] mLastTileState=$int1, Callback=$str2." }
+            { "[$str1] mLastTileState=$int1, Callback=$str2." },
         )
     }
 
@@ -198,7 +212,7 @@
         tileSpec: String,
         state: Int,
         disabledByPolicy: Boolean,
-        color: Int
+        color: Int,
     ) {
         // This method is added to further debug b/250618218 which has only been observed from the
         // InternetTile, so we are only logging the background color change for the InternetTile
@@ -215,7 +229,7 @@
                 bool1 = disabledByPolicy
                 int2 = color
             },
-            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }
+            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." },
         )
     }
 
@@ -229,7 +243,7 @@
                 str3 = state.icon?.toString()
                 int1 = state.state
             },
-            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." }
+            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." },
         )
     }
 
@@ -241,7 +255,7 @@
                 str1 = containerName
                 bool1 = expanded
             },
-            { "$str1 expanded=$bool1" }
+            { "$str1 expanded=$bool1" },
         )
     }
 
@@ -253,7 +267,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewAttached: $str1 orientation $int1" }
+            { "onViewAttached: $str1 orientation $int1" },
         )
     }
 
@@ -265,7 +279,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewDetached: $str1 orientation $int1" }
+            { "onViewDetached: $str1 orientation $int1" },
         )
     }
 
@@ -276,7 +290,7 @@
         newShouldUseSplitShade: Boolean,
         oldScreenLayout: Int,
         newScreenLayout: Int,
-        containerName: String
+        containerName: String,
     ) {
         configChangedBuffer.log(
             TAG,
@@ -297,7 +311,7 @@
                     "screen layout=${toScreenLayoutString(long1.toInt())} " +
                     "(was ${toScreenLayoutString(long2.toInt())}), " +
                     "splitShade=$bool2 (was $bool1)"
-            }
+            },
         )
     }
 
@@ -305,7 +319,7 @@
         after: Boolean,
         before: Boolean,
         force: Boolean,
-        containerName: String
+        containerName: String,
     ) {
         buffer.log(
             TAG,
@@ -316,7 +330,7 @@
                 bool2 = before
                 bool3 = force
             },
-            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }
+            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" },
         )
     }
 
@@ -328,7 +342,7 @@
                 int1 = tilesPerPageCount
                 int2 = totalTilesCount
             },
-            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }
+            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" },
         )
     }
 
@@ -340,7 +354,7 @@
                 str1 = tileName
                 int1 = pageIndex
             },
-            { "Adding $str1 to page number $int1" }
+            { "Adding $str1 to page number $int1" },
         )
     }
 
@@ -361,7 +375,7 @@
                 str1 = viewName
                 str2 = toVisibilityString(visibility)
             },
-            { "$str1 visibility: $str2" }
+            { "$str1 visibility: $str2" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 7c62e59..e0f0b6a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -20,14 +20,18 @@
 import androidx.compose.foundation.gestures.detectHorizontalDragGestures
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.systemGestureExclusion
 import androidx.compose.material3.LocalMinimumInteractiveComponentSize
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
 import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize
 
 /**
@@ -52,14 +56,18 @@
         // not receive the touch input accidentally.
         val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current
         Box(
-            Modifier.size(minTouchTargetSize).pointerInput(Unit) {
-                detectHorizontalDragGestures(
-                    onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
-                    onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } },
-                    onDragEnd = selectionState::onResizingDragEnd,
-                    onDragCancel = selectionState::onResizingDragEnd,
-                )
-            }
+            Modifier.size(minTouchTargetSize)
+                .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) }
+                .pointerInput(Unit) {
+                    detectHorizontalDragGestures(
+                        onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
+                        onDragStart = {
+                            tileWidths()?.let { selectionState.onResizingDragStart(it) }
+                        },
+                        onDragEnd = selectionState::onResizingDragEnd,
+                        onDragCancel = selectionState::onResizingDragEnd,
+                    )
+                }
         ) {
             ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 5ea8c21..a4f3c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.qsNewTiles;
 import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
 
 import android.animation.Animator;
@@ -66,12 +67,22 @@
 
     private ValueAnimator mColorAnimator = new ValueAnimator();
 
+    private int mColorUnavailable;
+    private int mColorInactive;
+    private int mColorActive;
+
     public QSIconViewImpl(Context context) {
         super(context);
 
         final Resources res = context.getResources();
         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size);
 
+        if (qsNewTiles()) { // pre-load icon tint colors
+            mColorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline);
+            mColorInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
+            mColorActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
+        }
+
         mIcon = createIcon();
         addView(mIcon);
         mColorAnimator.setDuration(QS_ANIM_LENGTH);
@@ -195,7 +206,11 @@
     }
 
     protected int getColor(QSTile.State state) {
-        return getIconColorForState(getContext(), state);
+        if (qsNewTiles()) {
+            return getCachedIconColorForState(state);
+        } else {
+            return getIconColorForState(getContext(), state);
+        }
     }
 
     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
@@ -267,6 +282,19 @@
         }
     }
 
+    private int getCachedIconColorForState(QSTile.State state) {
+        if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
+            return mColorUnavailable;
+        } else if (state.state == Tile.STATE_INACTIVE) {
+            return mColorInactive;
+        } else if (state.state == Tile.STATE_ACTIVE) {
+            return mColorActive;
+        } else {
+            Log.e("QSIconView", "Invalid state " + state);
+            return 0;
+        }
+    }
+
     private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter {
         private Runnable mRunnable;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6a8cc17..18b1f07 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -126,12 +126,12 @@
     private val overlayColorActive =
         Utils.applyAlpha(
             /* alpha= */ 0.11f,
-            Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
+            Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
         )
     private val overlayColorInactive =
         Utils.applyAlpha(
             /* alpha= */ 0.08f,
-            Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
+            Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive),
         )
 
     private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
@@ -188,10 +188,7 @@
     private var lastState = INVALID
     private var lastIconTint = 0
     private val launchableViewDelegate =
-        LaunchableViewDelegate(
-            this,
-            superSetVisibility = { super.setVisibility(it) },
-        )
+        LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })
     private var lastDisabledByPolicy = false
 
     private val locInScreen = IntArray(2)
@@ -418,7 +415,7 @@
             initLongPressEffectCallback()
             init(
                 { _: View -> longPressEffect.onTileClick() },
-                null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
+                { _: View -> true }, // Haptics and long-clicks are handled by [QSLongPressEffect]
             )
         } else {
             val expandable = Expandable.fromView(this)
@@ -583,7 +580,7 @@
                     AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
                     resources.getString(
                         R.string.accessibility_tile_disabled_by_policy_action_description
-                    )
+                    ),
                 )
             )
         } else {
@@ -591,7 +588,7 @@
                 info.addAction(
                     AccessibilityNodeInfo.AccessibilityAction(
                         AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
-                        resources.getString(R.string.accessibility_long_click_tile)
+                        resources.getString(R.string.accessibility_long_click_tile),
                     )
                 )
             }
@@ -646,7 +643,6 @@
     }
 
     // HANDLE STATE CHANGES RELATED METHODS
-
     protected open fun handleStateChanged(state: QSTile.State) {
         val allowAnimations = animationsEnabled()
         isClickable = state.state != Tile.STATE_UNAVAILABLE
@@ -716,35 +712,35 @@
                 state.spec,
                 state.state,
                 state.disabledByPolicy,
-                getBackgroundColorForState(state.state, state.disabledByPolicy)
+                getBackgroundColorForState(state.state, state.disabledByPolicy),
             )
             if (allowAnimations) {
                 singleAnimator.setValues(
                     colorValuesHolder(
                         BACKGROUND_NAME,
                         backgroundColor,
-                        getBackgroundColorForState(state.state, state.disabledByPolicy)
+                        getBackgroundColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         LABEL_NAME,
                         label.currentTextColor,
-                        getLabelColorForState(state.state, state.disabledByPolicy)
+                        getLabelColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         SECONDARY_LABEL_NAME,
                         secondaryLabel.currentTextColor,
-                        getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
+                        getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         CHEVRON_NAME,
                         chevronView.imageTintList?.defaultColor ?: 0,
-                        getChevronColorForState(state.state, state.disabledByPolicy)
+                        getChevronColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         OVERLAY_NAME,
                         backgroundOverlayColor,
-                        getOverlayColorForState(state.state)
-                    )
+                        getOverlayColorForState(state.state),
+                    ),
                 )
                 singleAnimator.start()
             } else {
@@ -753,7 +749,7 @@
                     getLabelColorForState(state.state, state.disabledByPolicy),
                     getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
                     getChevronColorForState(state.state, state.disabledByPolicy),
-                    getOverlayColorForState(state.state)
+                    getOverlayColorForState(state.state),
                 )
             }
         }
@@ -1077,7 +1073,7 @@
             backgroundColor,
             label.currentTextColor,
             secondaryLabel.currentTextColor,
-            chevronView.imageTintList?.defaultColor ?: 0
+            chevronView.imageTintList?.defaultColor ?: 0,
         )
 
     inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 5ea9e6a..301ab2b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -311,10 +311,7 @@
         mConfig = MobileMappings.Config.readConfig(mContext);
         mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
         mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
-        InternetTelephonyCallback telephonyCallback =
-                new InternetTelephonyCallback(mDefaultDataSubId);
-        mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback);
-        mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+        registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId);
         // Listen the connectivity changes
         mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback);
         mCanConfigWifi = canConfigWifi;
@@ -346,6 +343,23 @@
         mCallback = null;
     }
 
+    /**
+     * This is to generate and register the new callback to Telephony for uncached subscription id,
+     * then cache it. Telephony also cached this callback into
+     * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached
+     * already, it shall do nothing to avoid registering redundant callback to Telephony.
+     */
+    private void registerInternetTelephonyCallback(
+            TelephonyManager telephonyManager, int subId) {
+        if (mSubIdTelephonyCallbackMap.containsKey(subId)) {
+            // Avoid to generate and register unnecessary callback to Telephony.
+            return;
+        }
+        InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
+        mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+        telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+    }
+
     boolean isAirplaneModeEnabled() {
         return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
     }
@@ -673,9 +687,7 @@
             int subId = subInfo.getSubscriptionId();
             if (mSubIdTelephonyManagerMap.get(subId) == null) {
                 TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId);
-                InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
-                secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback);
-                mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+                registerInternetTelephonyCallback(secondaryTm, subId);
                 mSubIdTelephonyManagerMap.put(subId, secondaryTm);
             }
             return subId;
@@ -1351,6 +1363,7 @@
         if (DEBUG) {
             Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId);
         }
+
         if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) {
             // clean up old defaultDataSubId
             TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId);
@@ -1366,9 +1379,7 @@
             // create for new defaultDataSubId
             mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId);
             mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager);
-            InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId);
-            mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback);
-            mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback);
+            registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId);
             mCallback.onSubscriptionsChanged(defaultDataSubId);
         }
         mDefaultDataSubId = defaultDataSubId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
index 8965ef2..bb0b9b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import android.content.res.Resources
+import android.os.Handler
 import android.widget.Switch
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -28,6 +30,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import javax.inject.Inject
 
 /** Maps [InternetTileModel] to [QSTileState]. */
@@ -37,6 +40,7 @@
     @Main private val resources: Resources,
     private val theme: Resources.Theme,
     private val context: Context,
+    @Main private val handler: Handler,
 ) : QSTileDataToStateMapper<InternetTileModel> {
 
     override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
@@ -44,25 +48,42 @@
             label = resources.getString(R.string.quick_settings_internet_label)
             expandedAccessibilityClass = Switch::class
 
-            if (data.secondaryLabel != null) {
-                secondaryLabel = data.secondaryLabel.loadText(context)
-            } else {
-                secondaryLabel = data.secondaryTitle
-            }
+            secondaryLabel =
+                if (data.secondaryLabel != null) {
+                    data.secondaryLabel.loadText(context)
+                } else {
+                    data.secondaryTitle
+                }
 
             stateDescription = data.stateDescription.loadContentDescription(context)
             contentDescription = data.contentDescription.loadContentDescription(context)
 
-            iconRes = data.iconId
-            if (data.icon != null) {
-                this.icon = { data.icon }
-            } else if (data.iconId != null) {
-                val loadedIcon =
-                    Icon.Loaded(
-                        resources.getDrawable(data.iconId!!, theme),
-                        contentDescription = null
-                    )
-                this.icon = { loadedIcon }
+            when (val dataIcon = data.icon) {
+                is InternetTileIconModel.ResourceId -> {
+                    iconRes = dataIcon.resId
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resId, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
+
+                is InternetTileIconModel.Cellular -> {
+                    val signalDrawable = SignalDrawable(context, handler)
+                    signalDrawable.setLevel(dataIcon.level)
+                    icon = { Icon.Loaded(signalDrawable, contentDescription = null) }
+                }
+
+                is InternetTileIconModel.Satellite -> {
+                    iconRes = dataIcon.resourceIcon.res // level is inferred from res
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resourceIcon.res, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
             }
 
             sideViewIcon = QSTileState.SideViewIcon.Chevron
@@ -75,7 +96,7 @@
                 setOf(
                     QSTileState.UserAction.CLICK,
                     QSTileState.UserAction.TOGGLE_CLICK,
-                    QSTileState.UserAction.LONG_CLICK
+                    QSTileState.UserAction.LONG_CLICK,
                 )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
index 204ead3..6fe3979 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -20,13 +20,10 @@
 import android.content.Context
 import android.os.UserHandle
 import android.text.Html
-import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
 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.internet.domain.model.InternetTileModel
@@ -36,12 +33,12 @@
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.utils.coroutines.flow.mapLatestConflated
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -51,7 +48,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
 /** Observes internet state changes providing the [InternetTileModel]. */
@@ -59,7 +55,6 @@
 @Inject
 constructor(
     private val context: Context,
-    @Main private val mainCoroutineContext: CoroutineContext,
     @Application private val scope: CoroutineScope,
     airplaneModeRepository: AirplaneModeRepository,
     private val connectivityRepository: ConnectivityRepository,
@@ -79,8 +74,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryTitle = secondary,
-                        iconId = wifiIcon.icon.res,
-                        icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+                        icon = InternetTileIconModel.ResourceId(wifiIcon.icon.res),
                         stateDescription = wifiIcon.contentDescription,
                         contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
                     )
@@ -116,11 +110,10 @@
             if (it == null) {
                 notConnectedFlow
             } else {
-                combine(
-                        it.networkName,
-                        it.signalLevelIcon,
-                        mobileDataContentName,
-                    ) { networkNameModel, signalIcon, dataContentDescription ->
+                combine(it.networkName, it.signalLevelIcon, mobileDataContentName) {
+                        networkNameModel,
+                        signalIcon,
+                        dataContentDescription ->
                         Triple(networkNameModel, signalIcon, dataContentDescription)
                     }
                     .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) ->
@@ -129,17 +122,12 @@
                                 val secondary =
                                     mobileDataContentConcat(
                                         networkNameModel.name,
-                                        dataContentDescription
+                                        dataContentDescription,
                                     )
 
-                                val drawable =
-                                    withContext(mainCoroutineContext) { SignalDrawable(context) }
-                                drawable.setLevel(signalIcon.level)
-                                val loadedIcon = Icon.Loaded(drawable, null)
-
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    icon = loadedIcon,
+                                    icon = InternetTileIconModel.Cellular(signalIcon.level),
                                     stateDescription =
                                         ContentDescription.Loaded(secondary.toString()),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
@@ -150,9 +138,10 @@
                                     signalIcon.icon.contentDescription.loadContentDescription(
                                         context
                                     )
+
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    iconId = signalIcon.icon.res,
+                                    icon = InternetTileIconModel.Satellite(signalIcon.icon),
                                     stateDescription = ContentDescription.Loaded(secondary),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
                                 )
@@ -164,7 +153,7 @@
 
     private fun mobileDataContentConcat(
         networkName: String?,
-        dataContentDescription: CharSequence?
+        dataContentDescription: CharSequence?,
     ): CharSequence {
         if (dataContentDescription == null) {
             return networkName ?: ""
@@ -177,9 +166,9 @@
             context.getString(
                 R.string.mobile_carrier_text_format,
                 networkName,
-                dataContentDescription
+                dataContentDescription,
             ),
-            0
+            0,
         )
     }
 
@@ -199,7 +188,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryLabel = secondary?.toText(),
-                        iconId = it.res,
+                        icon = InternetTileIconModel.ResourceId(it.res),
                         stateDescription = null,
                         contentDescription = secondary,
                     )
@@ -208,16 +197,18 @@
         }
 
     private val notConnectedFlow: StateFlow<InternetTileModel> =
-        combine(
-                wifiInteractor.areNetworksAvailable,
-                airplaneModeRepository.isAirplaneMode,
-            ) { networksAvailable, isAirplaneMode ->
+        combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) {
+                networksAvailable,
+                isAirplaneMode ->
                 when {
                     isAirplaneMode -> {
                         val secondary = context.getString(R.string.status_bar_airplane)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_unavailable,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_unavailable
+                                ),
                             stateDescription = null,
                             contentDescription = ContentDescription.Loaded(secondary),
                         )
@@ -227,10 +218,13 @@
                             context.getString(R.string.quick_settings_networks_available)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_available,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_available
+                                ),
                             stateDescription = null,
                             contentDescription =
-                                ContentDescription.Loaded("$internetLabel,$secondary")
+                                ContentDescription.Loaded("$internetLabel,$secondary"),
                         )
                     }
                     else -> {
@@ -248,7 +242,7 @@
      */
     override fun tileData(
         user: UserHandle,
-        triggers: Flow<DataUpdateTrigger>
+        triggers: Flow<DataUpdateTrigger>,
     ): Flow<InternetTileModel> =
         connectivityRepository.defaultConnections.flatMapLatest {
             when {
@@ -265,7 +259,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
index ece90461..15b4e47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
@@ -17,23 +17,21 @@
 package com.android.systemui.qs.tiles.impl.internet.domain.model
 
 import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 
 /** Model describing the state that the QS Internet tile should be in. */
 sealed interface InternetTileModel {
     val secondaryTitle: CharSequence?
     val secondaryLabel: Text?
-    val iconId: Int?
-    val icon: Icon?
+    val icon: InternetTileIconModel
     val stateDescription: ContentDescription?
     val contentDescription: ContentDescription?
 
     data class Active(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
@@ -41,8 +39,7 @@
     data class Inactive(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
index 3b97d82..afb9a78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
@@ -16,11 +16,18 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 /**
  * Models UI state used to render the content of the quick settings shade overlay.
@@ -31,15 +38,43 @@
 class QuickSettingsShadeOverlayContentViewModel
 @AssistedInject
 constructor(
+    val shadeInteractor: ShadeInteractor,
     val sceneInteractor: SceneInteractor,
     val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
     val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
-) {
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                sceneInteractor.currentScene.collect { currentScene ->
+                    when (currentScene) {
+                        // TODO(b/369513770): The ShadeSession should be preserved in this scenario.
+                        Scenes.Bouncer ->
+                            shadeInteractor.collapseQuickSettingsShade(
+                                loggingReason = "bouncer shown while shade is open"
+                            )
+                    }
+                }
+            }
+
+            launch {
+                shadeInteractor.isShadeTouchable
+                    .distinctUntilChanged()
+                    .filter { !it }
+                    .collect {
+                        shadeInteractor.collapseQuickSettingsShade(
+                            loggingReason = "device became non-interactive"
+                        )
+                    }
+            }
+        }
+
+        awaitCancellation()
+    }
+
     fun onScrimClicked() {
-        sceneInteractor.hideOverlay(
-            overlay = Overlays.QuickSettingsShade,
-            loggingReason = "Shade scrim clicked",
-        )
+        shadeInteractor.collapseQuickSettingsShade(loggingReason = "shade scrim clicked")
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index ac49e91..559c263 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -248,7 +248,8 @@
                         } else if (action == ACTION_UP) {
                             // Gesture was too short to be picked up by scene container touch
                             // handling; programmatically start the transition to the shade.
-                            mShadeInteractor.get().expandNotificationShade("short launcher swipe");
+                            mShadeInteractor.get()
+                                    .expandNotificationsShade("short launcher swipe", null);
                         }
                     }
                     event.recycle();
@@ -265,7 +266,8 @@
                         mSceneInteractor.get().onRemoteUserInputStarted(
                                 "trackpad swipe");
                     } else if (action == ACTION_UP) {
-                        mShadeInteractor.get().expandNotificationShade("short trackpad swipe");
+                        mShadeInteractor.get()
+                                .expandNotificationsShade("short trackpad swipe", null);
                     }
                     mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event);
                 } else {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index a5f4a89..4d2bc91 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -61,11 +61,11 @@
         uiEventLogger,
         notificationManager,
         userContextProvider,
-        keyguardDismissUtil
+        keyguardDismissUtil,
     ) {
 
-    private val commandHandler =
-        IssueRecordingServiceCommandHandler(
+    private val session =
+        IssueRecordingServiceSession(
             bgExecutor,
             dialogTransitionAnimator,
             panelInteractor,
@@ -86,7 +86,7 @@
         Log.d(getTag(), "handling action: ${intent?.action}")
         when (intent?.action) {
             ACTION_START -> {
-                commandHandler.handleStartCommand()
+                session.start()
                 if (!issueRecordingState.recordScreen) {
                     // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
                     // will circumvent the RecordingService's screen recording start code.
@@ -94,12 +94,12 @@
                 }
             }
             ACTION_STOP,
-            ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver)
+            ACTION_STOP_NOTIF -> session.stop(contentResolver)
             ACTION_SHARE -> {
-                commandHandler.handleShareCommand(
+                session.share(
                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
                     intent.getParcelableExtra(EXTRA_PATH, Uri::class.java),
-                    this
+                    this,
                 )
                 // Unlike all other actions, action_share has different behavior for the screen
                 // recording qs tile than it does for the record issue qs tile. Return sticky to
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
rename to packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
index 32de0f3..e4d3e6c 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
@@ -34,9 +34,11 @@
 /**
  * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android
  * specifically calls out that there is no supported way to test IntentServices here:
- * https://developer.android.com/training/testing/other-components/services
+ * https://developer.android.com/training/testing/other-components/services, and mentions that the
+ * best way to add unit tests, is to introduce a separate class containing the business logic of
+ * that service, and test the functionality via that class.
  */
-class IssueRecordingServiceCommandHandler(
+class IssueRecordingServiceSession(
     private val bgExecutor: Executor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val panelInteractor: PanelInteractor,
@@ -47,12 +49,12 @@
     private val userContextProvider: UserContextProvider,
 ) {
 
-    fun handleStartCommand() {
+    fun start() {
         bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) }
         issueRecordingState.isRecording = true
     }
 
-    fun handleStopCommand(contentResolver: ContentResolver) {
+    fun stop(contentResolver: ContentResolver) {
         bgExecutor.execute {
             if (issueRecordingState.traceConfig.longTrace) {
                 Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED)
@@ -62,12 +64,12 @@
         issueRecordingState.isRecording = false
     }
 
-    fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) {
+    fun share(notificationId: Int, screenRecording: Uri?, context: Context) {
         bgExecutor.execute {
             notificationManager.cancelAsUser(
                 null,
                 notificationId,
-                UserHandle(userContextProvider.userContext.userId)
+                UserHandle(userContextProvider.userContext.userId),
             )
 
             if (issueRecordingState.takeBugreport) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
new file mode 100644
index 0000000..a8d0777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.scene.data.repository
+
+import android.graphics.Region
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class SystemGestureExclusionRepository
+@Inject
+constructor(private val windowManager: IWindowManager) {
+
+    /**
+     * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+     * identified with [displayId].
+     */
+    fun exclusionRegion(displayId: Int): Flow<Region?> {
+        return conflatedCallbackFlow {
+            val listener =
+                object : ISystemGestureExclusionListener.Stub() {
+                    override fun onSystemGestureExclusionChanged(
+                        displayId: Int,
+                        restrictedRegion: Region?,
+                        unrestrictedRegion: Region?,
+                    ) {
+                        trySend(restrictedRegion)
+                    }
+                }
+            windowManager.registerSystemGestureExclusionListener(listener, displayId)
+
+            awaitClose {
+                windowManager.unregisterSystemGestureExclusionListener(listener, displayId)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
new file mode 100644
index 0000000..4cee874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.scene.domain.interactor
+
+import android.graphics.Region
+import com.android.systemui.scene.data.repository.SystemGestureExclusionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+class SystemGestureExclusionInteractor
+@Inject
+constructor(private val repository: SystemGestureExclusionRepository) {
+
+    /**
+     * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+     * identified with [displayId].
+     */
+    fun exclusionRegion(displayId: Int): Flow<Region?> {
+        return repository.exclusionRegion(displayId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
index b9f57f2..3c6d858 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
@@ -32,4 +32,7 @@
      * normal collapse would.
      */
     val SlightlyFasterShadeCollapse = TransitionKey("SlightlyFasterShadeCollapse")
+
+    /** Reference to a content transition that should happen instantly, i.e. without animation. */
+    val Instant = TransitionKey("Instant")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 7f35d73..a7e7d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -107,7 +107,9 @@
             view.viewModel(
                 traceName = "SceneWindowRootViewBinder",
                 minWindowLifecycleState = WindowLifecycleState.ATTACHED,
-                factory = { viewModelFactory.create(motionEventHandlerReceiver) },
+                factory = {
+                    viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+                },
             ) { viewModel ->
                 try {
                     view.setViewTreeOnBackPressedDispatcherOwner(
@@ -184,7 +186,7 @@
                 PlatformTheme {
                     ScreenDecorProvider(
                         displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
-                        screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+                        screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context),
                     ) {
                         SceneContainer(
                             viewModel = viewModel,
@@ -205,9 +207,7 @@
     ): ComposeView {
         return ComposeView(context).apply {
             setContent {
-                AlternateBouncer(
-                    alternateBouncerDependencies = alternateBouncerDependencies,
-                )
+                AlternateBouncer(alternateBouncerDependencies = alternateBouncerDependencies)
             }
         }
     }
@@ -234,14 +234,7 @@
                         else -> CutoutLocation.CENTER
                     }
                 val viewDisplayCutout = it?.displayCutout
-                DisplayCutout(
-                    left,
-                    top,
-                    right,
-                    bottom,
-                    location,
-                    viewDisplayCutout,
-                )
+                DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
new file mode 100644
index 0000000..a1d915a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.scene.domain.interactor.SystemGestureExclusionInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToInt
+
+/** Decides whether drag gestures should be filtered out in the scene container framework. */
+class SceneContainerGestureFilter
+@AssistedInject
+constructor(interactor: SystemGestureExclusionInteractor, @Assisted displayId: Int) :
+    ExclusiveActivatable() {
+
+    private val hydrator = Hydrator("SceneContainerGestureFilter.hydrator")
+    private val exclusionRegion by
+        hydrator.hydratedStateOf(
+            traceName = "exclusionRegion",
+            initialValue = null,
+            source = interactor.exclusionRegion(displayId),
+        )
+
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
+
+    /**
+     * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+     * ignored, `false` otherwise.
+     *
+     * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+     * gesture.
+     */
+    fun shouldFilterGesture(startPosition: Offset): Boolean {
+        check(isActive) { "Must be activated to use!" }
+
+        return exclusionRegion?.contains(startPosition.x.roundToInt(), startPosition.y.roundToInt())
+            ?: false
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(displayId: Int): SceneContainerGestureFilter
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index af1f5a7..0bf2d49 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.view.MotionEvent
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.compose.animation.scene.ObservableTransitionState
@@ -41,9 +42,12 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 
 /** Models UI state for the scene container. */
 class SceneContainerViewModel
@@ -55,6 +59,8 @@
     shadeInteractor: ShadeInteractor,
     private val splitEdgeDetector: SplitEdgeDetector,
     private val logger: SceneLogger,
+    gestureFilterFactory: SceneContainerGestureFilter.Factory,
+    @Assisted displayId: Int,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
 ) : ExclusiveActivatable() {
 
@@ -80,6 +86,8 @@
                 },
         )
 
+    private val gestureFilter: SceneContainerGestureFilter = gestureFilterFactory.create(displayId)
+
     override suspend fun onActivated(): Nothing {
         try {
             // Sends a MotionEventHandler to the owner of the view-model so they can report
@@ -96,7 +104,11 @@
                 }
             )
 
-            hydrator.activate()
+            coroutineScope {
+                launch { hydrator.activate() }
+                launch { gestureFilter.activate() }
+            }
+            awaitCancellation()
         } finally {
             // Clears the previously-sent MotionEventHandler so the owner of the view-model releases
             // their reference to it.
@@ -243,6 +255,17 @@
         }
     }
 
+    /**
+     * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+     * ignored, `false` otherwise.
+     *
+     * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+     * gesture.
+     */
+    fun shouldFilterGesture(startPosition: Offset): Boolean {
+        return gestureFilter.shouldFilterGesture(startPosition)
+    }
+
     /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */
     interface MotionEventHandler {
         /** Notifies that a [MotionEvent] has occurred. */
@@ -258,7 +281,8 @@
     @AssistedFactory
     interface Factory {
         fun create(
-            motionEventHandlerReceiver: (MotionEventHandler?) -> Unit
+            displayId: Int,
+            motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
         ): SceneContainerViewModel
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 361226a4..6c99282 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -22,8 +22,8 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -80,18 +80,25 @@
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun isShadeEnabled() = shadeInteractor.isShadeEnabled.value
 
+    @Deprecated("Deprecated in Java")
     override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value
 
+    @Deprecated("Deprecated in Java")
     override fun isExpandingOrCollapsing(): Boolean = shadeInteractor.isUserInteracting.value
 
+    @Deprecated("Deprecated in Java")
     override fun instantExpandShade() {
         // Do nothing
     }
 
     override fun instantCollapseShade() {
-        sceneInteractor.snapToScene(SceneFamilies.Home, "hide shade")
+        shadeInteractor.collapseNotificationsShade(
+            loggingReason = "ShadeControllerSceneImpl.instantCollapseShade",
+            transitionKey = Instant,
+        )
     }
 
     override fun animateCollapseShade(
@@ -122,16 +129,17 @@
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun collapseWithDuration(animationDuration: Int) {
         // TODO(b/300258424) inline this. The only caller uses the default duration.
         animateCollapseShade()
     }
 
     private fun animateCollapseShadeInternal() {
-        sceneInteractor.changeScene(
-            SceneFamilies.Home, // TODO(b/336581871): add sceneState?
-            "ShadeController.animateCollapseShade",
-            SlightlyFasterShadeCollapse,
+        // TODO(b/336581871): add sceneState?
+        shadeInteractor.collapseEitherShade(
+            loggingReason = "ShadeController.animateCollapseShade",
+            transitionKey = SlightlyFasterShadeCollapse,
         )
     }
 
@@ -140,6 +148,7 @@
         animateCollapseShade()
     }
 
+    @Deprecated("Deprecated in Java")
     override fun closeShadeIfOpen(): Boolean {
         if (shadeInteractor.isAnyExpanded.value) {
             commandQueue.animateCollapsePanels(
@@ -155,6 +164,7 @@
         animateCollapseShadeForcedDelayed()
     }
 
+    @Deprecated("Deprecated in Java")
     override fun collapseShade(animate: Boolean) {
         if (animate) {
             animateCollapseShade()
@@ -163,13 +173,14 @@
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun collapseOnMainThread() {
         // TODO if this works with delegation alone, we can deprecate and delete
         collapseShade()
     }
 
     override fun expandToNotifications() {
-        shadeInteractor.expandNotificationShade("ShadeController.animateExpandShade")
+        shadeInteractor.expandNotificationsShade("ShadeController.animateExpandShade")
     }
 
     override fun expandToQs() {
@@ -193,14 +204,17 @@
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun postAnimateCollapseShade() {
         animateCollapseShade()
     }
 
+    @Deprecated("Deprecated in Java")
     override fun postAnimateForceCollapseShade() {
         animateCollapseShadeForced()
     }
 
+    @Deprecated("Deprecated in Java")
     override fun postAnimateExpandQs() {
         expandToQs()
     }
@@ -214,18 +228,23 @@
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun makeExpandedInvisible() {
         // Do nothing
     }
 
+    @Deprecated("Deprecated in Java")
     override fun makeExpandedVisible(force: Boolean) {
         // Do nothing
     }
 
+    @Deprecated("Deprecated in Java")
     override fun isExpandedVisible(): Boolean {
-        return sceneInteractor.currentScene.value != Scenes.Gone
+        return sceneInteractor.currentScene.value != Scenes.Gone ||
+            sceneInteractor.currentOverlays.value.isNotEmpty()
     }
 
+    @Deprecated("Deprecated in Java")
     override fun onStatusBarTouch(event: MotionEvent) {
         // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled
         throw UnsupportedOperationException()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index b046c50..a3f2c64 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import androidx.annotation.FloatRange
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.shade.shared.model.ShadeMode
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -27,19 +28,22 @@
 
 /** Business logic for shade interactions. */
 interface ShadeInteractor : BaseShadeInteractor {
-    /** Emits true if the shade is currently allowed and false otherwise. */
+    /** Emits true if the Notifications shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean>
 
-    /** Emits true if QS is currently allowed and false otherwise. */
+    /** Emits true if QS shade is currently allowed and false otherwise. */
     val isQsEnabled: StateFlow<Boolean>
 
-    /** Whether either the shade or QS is fully expanded. */
+    /** Whether either the Notifications shade or QS shade is fully expanded. */
     val isAnyFullyExpanded: StateFlow<Boolean>
 
-    /** Whether the Shade is fully expanded. */
+    /** Whether the Notifications Shade is fully expanded. */
     val isShadeFullyExpanded: Flow<Boolean>
 
-    /** Whether the Shade is fully collapsed. */
+    /** Whether Notifications Shade is expanded a non-zero amount. */
+    val isShadeAnyExpanded: StateFlow<Boolean>
+
+    /** Whether the Notifications Shade is fully collapsed. */
     val isShadeFullyCollapsed: Flow<Boolean>
 
     /**
@@ -102,7 +106,7 @@
      */
     val isAnyExpanded: StateFlow<Boolean>
 
-    /** The amount [0-1] that the shade has been opened. */
+    /** The amount [0-1] that the Notifications Shade has been opened. */
     val shadeExpansion: StateFlow<Float>
 
     /**
@@ -111,7 +115,7 @@
      */
     val qsExpansion: StateFlow<Float>
 
-    /** Whether Quick Settings is expanded a non-zero amount. */
+    /** Whether Quick Settings Shade is expanded a non-zero amount. */
     val isQsExpanded: StateFlow<Boolean>
 
     /**
@@ -142,16 +146,38 @@
     val isUserInteractingWithQs: Flow<Boolean>
 
     /**
-     * Triggers the expansion (opening) of the notification shade. If the notification shade is
-     * already open, this has no effect.
+     * Triggers the expansion (opening) of the notifications shade. If it is already expanded, this
+     * has no effect.
      */
-    fun expandNotificationShade(loggingReason: String)
+    fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null)
 
     /**
-     * Triggers the expansion (opening) of the quick settings shade. If the quick settings shade is
-     * already open, this has no effect.
+     * Triggers the expansion (opening) of the quick settings shade. If it is already expanded, this
+     * has no effect.
      */
-    fun expandQuickSettingsShade(loggingReason: String)
+    fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey? = null)
+
+    /**
+     * Triggers the collapse (closing) of the notifications shade. If it is already collapsed, this
+     * has no effect.
+     */
+    fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null)
+
+    /**
+     * Triggers the collapse (closing) of the quick settings shade. If it is already collapsed, this
+     * has no effect.
+     */
+    fun collapseQuickSettingsShade(
+        loggingReason: String,
+        transitionKey: TransitionKey? = null,
+        bypassNotificationsShade: Boolean = false,
+    )
+
+    /**
+     * Triggers the collapse (closing) of the notifications shade or quick settings shade, whichever
+     * is open. If both are already collapsed, this has no effect.
+     */
+    fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey? = null)
 }
 
 fun createAnyExpansionFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index fb14828..322fca3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade.domain.interactor
 
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
@@ -31,6 +32,7 @@
     override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
     override val isQsEnabled: StateFlow<Boolean> = inactiveFlowBoolean
     override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat
+    override val isShadeAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
     override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
     override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
     override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean
@@ -50,7 +52,17 @@
 
     override fun getTopEdgeSplitFraction(): Float = 0.5f
 
-    override fun expandNotificationShade(loggingReason: String) {}
+    override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
 
-    override fun expandQuickSettingsShade(loggingReason: String) {}
+    override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {}
+
+    override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
+
+    override fun collapseQuickSettingsShade(
+        loggingReason: String,
+        transitionKey: TransitionKey?,
+        bypassNotificationsShade: Boolean,
+    ) {}
+
+    override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {}
 }
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 3eab02a..949d2aa 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
@@ -78,12 +78,16 @@
     override val isShadeFullyExpanded: Flow<Boolean> =
         baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged()
 
+    override val isShadeAnyExpanded: StateFlow<Boolean> =
+        baseShadeInteractor.shadeExpansion
+            .map { it > 0 }
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
     override val isShadeFullyCollapsed: Flow<Boolean> =
         baseShadeInteractor.shadeExpansion.map { it <= 0f }.distinctUntilChanged()
 
     override val isUserInteracting: StateFlow<Boolean> =
         combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
-            .distinctUntilChanged()
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     override val isShadeTouchable: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index df09486..0902c39 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.app.tracing.FlowTracing.traceAsCounter
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -111,18 +112,40 @@
     override val isUserInteractingWithQs: Flow<Boolean> =
         userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
 
-    override fun expandNotificationShade(loggingReason: String) {
+    override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
         throw UnsupportedOperationException(
             "expandNotificationShade() is not supported in legacy shade"
         )
     }
 
-    override fun expandQuickSettingsShade(loggingReason: String) {
+    override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {
         throw UnsupportedOperationException(
             "expandQuickSettingsShade() is not supported in legacy shade"
         )
     }
 
+    override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
+        throw UnsupportedOperationException(
+            "collapseNotificationShade() is not supported in legacy shade"
+        )
+    }
+
+    override fun collapseQuickSettingsShade(
+        loggingReason: String,
+        transitionKey: TransitionKey?,
+        bypassNotificationsShade: Boolean,
+    ) {
+        throw UnsupportedOperationException(
+            "collapseQuickSettingsShade() is not supported in legacy shade"
+        )
+    }
+
+    override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {
+        throw UnsupportedOperationException(
+            "collapseEitherShade() is not supported in legacy shade"
+        )
+    }
+
     /**
      * Return a flow for whether a user is interacting with an expandable shade component using
      * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 81bf712..7658108 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -21,12 +21,16 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
@@ -133,43 +137,120 @@
             }
         }
 
-    override fun expandNotificationShade(loggingReason: String) {
+    override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
         if (shadeModeInteractor.isDualShade) {
             if (Overlays.QuickSettingsShade in sceneInteractor.currentOverlays.value) {
                 sceneInteractor.replaceOverlay(
                     from = Overlays.QuickSettingsShade,
                     to = Overlays.NotificationsShade,
                     loggingReason = loggingReason,
+                    transitionKey = transitionKey,
                 )
             } else {
                 sceneInteractor.showOverlay(
                     overlay = Overlays.NotificationsShade,
                     loggingReason = loggingReason,
+                    transitionKey = transitionKey,
                 )
             }
         } else {
-            sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = loggingReason)
+            sceneInteractor.changeScene(
+                toScene = Scenes.Shade,
+                loggingReason = loggingReason,
+                transitionKey =
+                    transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade },
+            )
         }
     }
 
-    override fun expandQuickSettingsShade(loggingReason: String) {
+    override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {
         if (shadeModeInteractor.isDualShade) {
             if (Overlays.NotificationsShade in sceneInteractor.currentOverlays.value) {
                 sceneInteractor.replaceOverlay(
                     from = Overlays.NotificationsShade,
                     to = Overlays.QuickSettingsShade,
                     loggingReason = loggingReason,
+                    transitionKey = transitionKey,
                 )
             } else {
                 sceneInteractor.showOverlay(
                     overlay = Overlays.QuickSettingsShade,
                     loggingReason = loggingReason,
+                    transitionKey = transitionKey,
                 )
             }
         } else {
+            val isSplitShade = shadeModeInteractor.isSplitShade
             sceneInteractor.changeScene(
-                toScene = Scenes.QuickSettings,
+                toScene = if (isSplitShade) Scenes.Shade else Scenes.QuickSettings,
                 loggingReason = loggingReason,
+                transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade },
+            )
+        }
+    }
+
+    override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
+        if (shadeModeInteractor.isDualShade) {
+            // TODO(b/356596436): Hide without animation if transitionKey is Instant.
+            sceneInteractor.hideOverlay(
+                overlay = Overlays.NotificationsShade,
+                loggingReason = loggingReason,
+                transitionKey = transitionKey,
+            )
+        } else if (transitionKey == Instant) {
+            // TODO(b/356596436): Define instant transition instead of snapToScene().
+            sceneInteractor.snapToScene(toScene = SceneFamilies.Home, loggingReason = loggingReason)
+        } else {
+            sceneInteractor.changeScene(
+                toScene = SceneFamilies.Home,
+                loggingReason = loggingReason,
+                transitionKey =
+                    transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade },
+            )
+        }
+    }
+
+    override fun collapseQuickSettingsShade(
+        loggingReason: String,
+        transitionKey: TransitionKey?,
+        bypassNotificationsShade: Boolean,
+    ) {
+        if (shadeModeInteractor.isDualShade) {
+            // TODO(b/356596436): Hide without animation if transitionKey is Instant.
+            sceneInteractor.hideOverlay(
+                overlay = Overlays.QuickSettingsShade,
+                loggingReason = loggingReason,
+                transitionKey = transitionKey,
+            )
+            return
+        }
+
+        val isSplitShade = shadeModeInteractor.isSplitShade
+        val targetScene =
+            if (bypassNotificationsShade || isSplitShade) SceneFamilies.Home else Scenes.Shade
+        if (transitionKey == Instant) {
+            // TODO(b/356596436): Define instant transition instead of snapToScene().
+            sceneInteractor.snapToScene(toScene = targetScene, loggingReason = loggingReason)
+        } else {
+            sceneInteractor.changeScene(
+                toScene = targetScene,
+                loggingReason = loggingReason,
+                transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade },
+            )
+        }
+    }
+
+    override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {
+        // Note: The notifications shade and QS shade may be both partially expanded simultaneously,
+        // so we don't use an 'else' clause here.
+        if (shadeExpansion.value > 0) {
+            collapseNotificationsShade(loggingReason = loggingReason, transitionKey = transitionKey)
+        }
+        if (isQsExpanded.value) {
+            collapseQuickSettingsShade(
+                loggingReason = loggingReason,
+                transitionKey = transitionKey,
+                bypassNotificationsShade = true,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 0fb3790..ea76ac4b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -21,10 +21,9 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -48,7 +47,7 @@
 
     @Deprecated("Use ShadeInteractor instead")
     override fun expandToNotifications() {
-        shadeInteractor.expandNotificationShade(
+        shadeInteractor.expandNotificationsShade(
             loggingReason = "ShadeLockscreenInteractorImpl.expandToNotifications"
         )
     }
@@ -71,17 +70,11 @@
     }
 
     override fun resetViews(animate: Boolean) {
-        val loggingReason = "ShadeLockscreenInteractorImpl.resetViews"
         // The existing comment to the only call to this claims it only calls it to collapse QS
-        if (shadeInteractor.shadeMode.value == ShadeMode.Dual) {
-            // TODO(b/356596436): Hide without animation if !animate.
-            sceneInteractor.hideOverlay(
-                overlay = Overlays.QuickSettingsShade,
-                loggingReason = loggingReason,
-            )
-        } else {
-            shadeInteractor.expandNotificationShade(loggingReason)
-        }
+        shadeInteractor.collapseQuickSettingsShade(
+            loggingReason = "ShadeLockscreenInteractorImpl.resetViews",
+            transitionKey = Instant.takeIf { !animate },
+        )
     }
 
     @Deprecated("Not supported by scenes")
@@ -93,7 +86,7 @@
         backgroundScope.launch {
             delay(delay)
             withContext(mainDispatcher) {
-                shadeInteractor.expandNotificationShade(
+                shadeInteractor.expandNotificationsShade(
                     "ShadeLockscreenInteractorImpl.transitionToExpandedShade"
                 )
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index caa4513..c838c37 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -55,6 +55,10 @@
     val isDualShade: Boolean
         get() = shadeMode.value is ShadeMode.Dual
 
+    /** Convenience shortcut for querying whether the current [shadeMode] is [ShadeMode.Split]. */
+    val isSplitShade: Boolean
+        get() = shadeMode.value is ShadeMode.Split
+
     /**
      * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
      * between "top-left" and "top-right" for the purposes of dual-shade invocation.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index a154e91..bd4ed5b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -29,9 +29,7 @@
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.privacy.PrivacyItem
 import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.shared.model.TransitionKeys
+import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
 import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -55,9 +53,8 @@
 class ShadeHeaderViewModel
 @AssistedInject
 constructor(
-    private val context: Context,
+    context: Context,
     private val activityStarter: ActivityStarter,
-    private val sceneInteractor: SceneInteractor,
     private val shadeInteractor: ShadeInteractor,
     private val mobileIconsInteractor: MobileIconsInteractor,
     val mobileIconsViewModel: MobileIconsViewModel,
@@ -120,7 +117,7 @@
                         map = { intent, _ ->
                             intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
                                 intent.action == Intent.ACTION_LOCALE_CHANGED
-                        }
+                        },
                     )
                     .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
                     .launchIn(this)
@@ -152,10 +149,9 @@
 
     /** Notifies that the system icons container was clicked. */
     fun onSystemIconContainerClicked() {
-        sceneInteractor.changeScene(
-            SceneFamilies.Home,
-            "ShadeHeaderViewModel.onSystemIconContainerClicked",
-            TransitionKeys.SlightlyFasterShadeCollapse,
+        shadeInteractor.collapseEitherShade(
+            loggingReason = "ShadeHeaderViewModel.onSystemIconContainerClicked",
+            transitionKey = SlightlyFasterShadeCollapse,
         )
     }
 
@@ -163,7 +159,7 @@
     fun onShadeCarrierGroupClicked() {
         activityStarter.postStartActivityDismissingKeyguard(
             Intent(Settings.ACTION_WIRELESS_SETTINGS),
-            0
+            0,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
similarity index 66%
copy from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
index 06f3db5..54a18f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
@@ -14,39 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.shared
+package com.android.systemui.statusbar.core
 
-import android.os.SystemProperties
 import com.android.systemui.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the minimalism prototype flag state. */
+/** Helper for reading or using the status bar connected displays flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object StatusBarConnectedDisplays {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS
 
     /** A token used for dependency declaration */
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the heads-up cycling animation enabled */
+    /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationMinimalismPrototype()
-
-    /**
-     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
-     * not grouped, but this property read allows that behavior to be disabled.
-     */
-    val ungroupTopUnseen: Boolean
-        get() =
-            if (isUnexpectedlyInLegacyMode()) false
-            else
-                SystemProperties.getBoolean(
-                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                    false
-                )
+        get() = Flags.statusBarConnectedDisplays()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
@@ -58,6 +45,14 @@
         RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
 
     /**
+     * Called to ensure code is only run when the flag is enabled. This will throw an exception if
+     * the flag is not enabled to ensure that the refactor author catches issues in testing.
+     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+    /**
      * Called to ensure code is only run when the flag is disabled. This will throw an exception if
      * the flag is enabled to ensure that the refactor author catches issues in testing.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 6907eef..1c840e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,8 +103,7 @@
     private boolean mTrackingHeadsUp;
     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
-    @VisibleForTesting
-    public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+    private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
     private boolean mIsShadeOrQsExpanded;
     private boolean mIsQsExpanded;
@@ -428,7 +427,7 @@
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
-                removeEntry(entry.getKey(), "allowReorder");
+                removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -631,8 +630,11 @@
             super.setEntry(entry, removeRunnable);
 
             if (NotificationThrottleHun.isEnabled()) {
-                mEntriesToRemoveWhenReorderingAllowed.add(entry);
-                if (!mVisualStabilityProvider.isReorderingAllowed()) {
+                if (!mVisualStabilityProvider.isReorderingAllowed()
+                        // We don't want to allow reordering while pulsing, but headsup need to
+                        // time out anyway
+                        && !entry.showingPulsing()) {
+                    mEntriesToRemoveWhenReorderingAllowed.add(entry);
                     entry.setSeenInShade(true);
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
deleted file mode 100644
index ec3c7d0..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2018 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.notification;
-
-import android.content.Intent;
-import android.view.View;
-
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-
-/**
- * Component responsible for handling actions on a notification which cause activites to start.
- * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
- */
-public interface NotificationActivityStarter {
-    /** Called when the user clicks on the notification bubble icon. */
-    void onNotificationBubbleIconClicked(NotificationEntry entry);
-
-    /** Called when the user clicks on the surface of a notification. */
-    void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
-
-    /** Called when the user clicks on a button in the notification guts which fires an intent. */
-    void startNotificationGutsIntent(Intent intent, int appUid,
-            ExpandableNotificationRow row);
-
-    /** Called when the user clicks "Manage" or "History" in the Shade. */
-    void startHistoryIntent(View view, boolean showHistory);
-
-    /** Called when the user succeed to drop notification to proper target view. */
-    void onDragSuccess(NotificationEntry entry);
-
-    default boolean isCollapsingToShowActivityOverLockscreen() {
-        return false;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
new file mode 100644
index 0000000..231a0b0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.notification
+
+import android.content.Intent
+import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
+import android.provider.Settings.ACTION_NOTIFICATION_HISTORY
+import android.provider.Settings.ACTION_NOTIFICATION_SETTINGS
+import android.provider.Settings.ACTION_ZEN_MODE_SETTINGS
+import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
+import android.view.View
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+
+/**
+ * Component responsible for handling actions on a notification which cause activites to start.
+ * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
+ */
+interface NotificationActivityStarter {
+
+    /** Called when the user clicks on the notification bubble icon. */
+    fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
+
+    /** Called when the user clicks on the surface of a notification. */
+    fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
+
+    /** Called when the user clicks on a button in the notification guts which fires an intent. */
+    fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
+
+    /**
+     * Called when the user clicks "Manage" or "History" in the Shade. Prefer using
+     * [startSettingsIntent] instead.
+     */
+    fun startHistoryIntent(view: View?, showHistory: Boolean)
+
+    /**
+     * Called to open a settings intent from a launchable view (such as the "Manage" or "History"
+     * button in the shade, or the "No notifications" text).
+     *
+     * @param view the view to perform the launch animation from (must extend [LaunchableView])
+     * @param intentInfo information about the (settings) intent to be launched
+     */
+    fun startSettingsIntent(view: View, intentInfo: SettingsIntent)
+
+    /** Called when the user succeed to drop notification to proper target view. */
+    fun onDragSuccess(entry: NotificationEntry?)
+
+    val isCollapsingToShowActivityOverLockscreen: Boolean
+        get() = false
+
+    /**
+     * Information about a settings intent to be launched.
+     *
+     * If the [targetIntent] is T and [backStack] is [A, B, C], the stack will look like
+     * [A, B, C, T].
+     */
+    data class SettingsIntent(
+        var targetIntent: Intent,
+        var backStack: List<Intent> = emptyList(),
+        var cujType: Int? = null,
+    ) {
+        // Utility factory methods for known intents
+        companion object {
+            fun forNotificationSettings(cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent = Intent(ACTION_NOTIFICATION_SETTINGS),
+                    cujType = cujType,
+                )
+
+            fun forNotificationHistory(cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent = Intent(ACTION_NOTIFICATION_HISTORY),
+                    backStack = listOf(Intent(ACTION_NOTIFICATION_SETTINGS)),
+                    cujType = cujType,
+                )
+
+            fun forModesSettings(cujType: Int? = null) =
+                SettingsIntent(targetIntent = Intent(ACTION_ZEN_MODE_SETTINGS), cujType = cujType)
+
+            fun forModeSettings(modeId: String, cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent =
+                        Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+                            .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId),
+                    backStack = listOf(Intent(ACTION_ZEN_MODE_SETTINGS)),
+                    cujType = cujType,
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index b342722..b67092c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -22,7 +22,7 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE
@@ -54,7 +54,7 @@
     fun getNotificationBuckets(): IntArray {
         if (
             PriorityPeopleSection.isEnabled ||
-                NotificationMinimalismPrototype.isEnabled ||
+                NotificationMinimalism.isEnabled ||
                 NotificationClassificationFlag.isEnabled
         ) {
             // We don't need this list to be adaptive, it can be the superset of all features.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index a621b2a..4e63b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
 import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
 import com.android.systemui.util.asIndenting
@@ -77,7 +77,7 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) {
             return
         }
         pipeline.addPromoter(unseenNotifPromoter)
@@ -129,26 +129,25 @@
             }
     }
 
-    private fun unseenFeatureEnabled(): Flow<Boolean> {
-        // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
-        //  Or should we actually just repurpose using the existing setting?
-        if (NotificationMinimalismPrototype.isEnabled) {
-            return flowOf(true)
+    private fun minimalismFeatureSettingEnabled(): Flow<Boolean> {
+        if (!NotificationMinimalism.isEnabled) {
+            return flowOf(false)
         }
-        return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
+        return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled()
     }
 
     private suspend fun trackUnseenFilterSettingChanges() {
-        unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+        // Only filter the seen notifs when the lock screen minimalism feature settings is on.
+        minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled ->
             // update local field and invalidate if necessary
-            if (isSettingEnabled != unseenFilterEnabled) {
-                unseenFilterEnabled = isSettingEnabled
+            if (isMinimalismSettingEnabled != unseenFilterEnabled) {
+                unseenFilterEnabled = isMinimalismSettingEnabled
                 unseenNotifications.clear()
                 unseenNotifPromoter.invalidateList("unseen setting changed")
             }
             // if the setting is enabled, then start tracking and filtering unseen notifications
-            logger.logTrackingUnseen(isSettingEnabled)
-            if (isSettingEnabled) {
+            logger.logTrackingUnseen(isMinimalismSettingEnabled)
+            if (isMinimalismSettingEnabled) {
                 trackSeenNotifications()
             }
         }
@@ -178,7 +177,7 @@
         }
 
     private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         if (!unseenFilterEnabled) return
         // Only ever elevate a top unseen notification on keyguard, not even locked shade
         if (statusBarStateController.state != StatusBarState.KEYGUARD) {
@@ -215,9 +214,9 @@
         object : NotifPromoter(TAG) {
             override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
                 when {
-                    NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+                    NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false
                     seenNotificationsInteractor.isTopOngoingNotification(child) -> true
-                    !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+                    !NotificationMinimalism.ungroupTopUnseen -> false
                     else -> seenNotificationsInteractor.isTopUnseenNotification(child)
                 }
         }
@@ -225,7 +224,7 @@
     val topOngoingSectioner =
         object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
             override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
                 }
@@ -235,7 +234,7 @@
     val topUnseenSectioner =
         object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
             override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
index e44a77c..a4fa729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -34,7 +34,10 @@
             TAG,
             LogLevel.DEBUG,
             messageInitializer = { bool1 = trackingUnseen },
-            messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+            messagePrinter = {
+                "${if (bool1) "Start" else "Stop"} " +
+                    "tracking unseen notifications because of settings change."
+            },
         )
 
     fun logShadeVisible(numUnseen: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 0bbde21..82ce31b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -25,7 +25,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -61,8 +60,27 @@
                 return false;
             }
 
-            if (!Flags.notificationsBackgroundIcons()) {
-                inflateOrUpdateIcons(entry);
+            switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
+                case STATE_ICONS_UNINFLATED:
+                    try {
+                        mIconManager.createIcons(entry);
+                        mIconsState.put(entry, STATE_ICONS_INFLATED);
+                    } catch (InflationException e) {
+                        reportInflationError(entry, e);
+                        mIconsState.put(entry, STATE_ICONS_ERROR);
+                    }
+                    break;
+                case STATE_ICONS_INFLATED:
+                    try {
+                        mIconManager.updateIcons(entry, /* usingCache = */ false);
+                    } catch (InflationException e) {
+                        reportInflationError(entry, e);
+                        mIconsState.put(entry, STATE_ICONS_ERROR);
+                    }
+                    break;
+                case STATE_ICONS_ERROR:
+                    // do nothing
+                    break;
             }
 
             return true;
@@ -72,19 +90,7 @@
     private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
         @Override
         public void onEntryInit(@NonNull NotificationEntry entry) {
-            // We default to STATE_ICONS_UNINFLATED anyway, so there's no need to initialize it.
-            if (!Flags.notificationsBackgroundIcons()) {
-                mIconsState.put(entry, STATE_ICONS_UNINFLATED);
-            }
-        }
-
-        @Override
-        public void onEntryAdded(@NonNull NotificationEntry entry) {
-            if (Flags.notificationsBackgroundIcons()) {
-                if (isMediaNotification(entry.getSbn())) {
-                    inflateOrUpdateIcons(entry);
-                }
-            }
+            mIconsState.put(entry, STATE_ICONS_UNINFLATED);
         }
 
         @Override
@@ -93,12 +99,6 @@
                 // The update may have fixed the inflation error, so give it another chance.
                 mIconsState.put(entry, STATE_ICONS_UNINFLATED);
             }
-
-            if (Flags.notificationsBackgroundIcons()) {
-                if (isMediaNotification(entry.getSbn())) {
-                    inflateOrUpdateIcons(entry);
-                }
-            }
         }
 
         @Override
@@ -107,31 +107,6 @@
         }
     };
 
-    private void inflateOrUpdateIcons(NotificationEntry entry) {
-        switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
-            case STATE_ICONS_UNINFLATED:
-                try {
-                    mIconManager.createIcons(entry);
-                    mIconsState.put(entry, STATE_ICONS_INFLATED);
-                } catch (InflationException e) {
-                    reportInflationError(entry, e);
-                    mIconsState.put(entry, STATE_ICONS_ERROR);
-                }
-                break;
-            case STATE_ICONS_INFLATED:
-                try {
-                    mIconManager.updateIcons(entry, /* usingCache = */ false);
-                } catch (InflationException e) {
-                    reportInflationError(entry, e);
-                    mIconsState.put(entry, STATE_ICONS_ERROR);
-                }
-                break;
-            case STATE_ICONS_ERROR:
-                // do nothing
-                break;
-        }
-    }
-
     private void reportInflationError(NotificationEntry entry, Exception e) {
         // This is the same logic as in PreparationCoordinator; it doesn't handle media
         // notifications when the media feature is enabled since they aren't displayed in the shade,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 73ce48b..96c260b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -88,11 +88,10 @@
         mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mCoordinators.add(lockScreenMinimalismCoordinator)
-        } else {
-            mCoordinators.add(unseenKeyguardCoordinator)
         }
+        mCoordinators.add(unseenKeyguardCoordinator)
         mCoordinators.add(rankingCoordinator)
         mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
@@ -125,11 +124,11 @@
         }
 
         // Manually add Ordered Sections
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
         }
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
         }
         mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index bfea2ba..cf1329c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.asIndenting
@@ -51,7 +50,6 @@
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -87,7 +85,6 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        NotificationMinimalismPrototype.assertInLegacyMode()
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
         scope.launch { trackUnseenFilterSettingChanges() }
@@ -253,10 +250,6 @@
     }
 
     private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (NotificationMinimalismPrototype.isEnabled) {
-            // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
-            return flowOf(false)
-        }
         return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index fe59d73..1fe32c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -45,17 +45,16 @@
 
 /**
  * How long to wait before auto-dismissing a notification that was kept for active remote input, and
- * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel
- * these given that they technically don't exist anymore. We wait a bit in case the app issues
- * an update, and to also give the other lifetime extenders a beat to decide they want it.
+ * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel these given
+ * that they technically don't exist anymore. We wait a bit in case the app issues an update, and to
+ * also give the other lifetime extenders a beat to decide they want it.
  */
 private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500
 
 /**
  * How long to wait before releasing a lifetime extension when requested to do so due to a user
- * interaction (such as tapping another action).
- * We wait a bit in case the app issues an update in response to the action, but not too long or we
- * risk appearing unresponsive to the user.
+ * interaction (such as tapping another action). We wait a bit in case the app issues an update in
+ * response to the action, but not too long or we risk appearing unresponsive to the user.
  */
 private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
 
@@ -63,22 +62,21 @@
 private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
 
 @CoordinatorScope
-class RemoteInputCoordinator @Inject constructor(
+class RemoteInputCoordinator
+@Inject
+constructor(
     dumpManager: DumpManager,
     private val mRebuilder: RemoteInputNotificationRebuilder,
     private val mNotificationRemoteInputManager: NotificationRemoteInputManager,
     @Main private val mMainHandler: Handler,
-    private val mSmartReplyController: SmartReplyController
+    private val mSmartReplyController: SmartReplyController,
 ) : Coordinator, RemoteInputListener, Dumpable {
 
     @VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender()
     @VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender()
     @VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender()
-    private val mRemoteInputLifetimeExtenders = listOf(
-            mRemoteInputHistoryExtender,
-            mSmartReplyHistoryExtender,
-            mRemoteInputActiveExtender
-    )
+    private val mRemoteInputLifetimeExtenders =
+        listOf(mRemoteInputHistoryExtender, mSmartReplyHistoryExtender, mRemoteInputActiveExtender)
 
     private lateinit var mNotifUpdater: InternalNotifUpdater
 
@@ -93,9 +91,7 @@
         if (lifetimeExtensionRefactor()) {
             pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
         } else {
-            mRemoteInputLifetimeExtenders.forEach {
-                pipeline.addNotificationLifetimeExtender(it)
-            }
+            mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
         }
         mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
         pipeline.addCollectionListener(mCollectionListener)
@@ -105,65 +101,75 @@
      * Listener that updates the appearance of the notification if it has been lifetime extended
      * by a a direct reply or a smart reply, and cancelled.
      */
-    val mCollectionListener = object : NotifCollectionListener {
-        override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
-            if (DEBUG) {
-                Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
-                        " fromSystem=$fromSystem)")
-            }
-            if (fromSystem) {
-                if (lifetimeExtensionRefactor()) {
-                    if ((entry.getSbn().getNotification().flags
-                                    and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
-                        if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
-                                        entry)) {
-                            val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
-                            entry.onRemoteInputInserted()
-                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                                    "Extending lifetime of notification with remote input")
-                        } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
-                                        entry)) {
-                            val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
-                            mSmartReplyController.stopSending(entry)
-                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                                    "Extending lifetime of notification with smart reply")
+    val mCollectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
+                if (DEBUG) {
+                    Log.d(
+                        TAG,
+                        "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
+                            " fromSystem=$fromSystem)",
+                    )
+                }
+                if (fromSystem) {
+                    if (lifetimeExtensionRefactor()) {
+                        if (
+                            (entry.getSbn().getNotification().flags and
+                                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+                        ) {
+                            // If we've received an update from the system and the entry is marked
+                            // as lifetime extended, that means system server has received a
+                            // cancelation in response to a direct reply, and sent an update to
+                            // let system ui know that it should rebuild the notification with
+                            // that direct reply.
+                            if (
+                                mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+                                    entry
+                                )
+                            ) {
+                                val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+                                mSmartReplyController.stopSending(entry)
+                                mNotifUpdater.onInternalNotificationUpdate(
+                                    newSbn,
+                                    "Extending lifetime of notification with smart reply",
+                                )
+                            } else {
+                                val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+                                entry.onRemoteInputInserted()
+                                mNotifUpdater.onInternalNotificationUpdate(
+                                    newSbn,
+                                    "Extending lifetime of notification with remote input",
+                                )
+                            }
                         } else {
-                            // The app may have re-cancelled a notification after it had already
-                            // been lifetime extended.
-                            // Rebuild the notification with the replies it already had to ensure
-                            // those replies continue to be displayed.
-                            val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
-                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                                    "Extending lifetime of notification that has already been " +
-                                            "lifetime extended.")
+                            // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+                            // should have their remote inputs list cleared.
+                            entry.remoteInputs = null
                         }
                     } else {
-                        // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
-                        // should have their remote inputs list cleared.
-                        entry.remoteInputs = null
+                        // Mark smart replies as sent whenever a notification is updated by the app,
+                        // otherwise the smart replies are never marked as sent.
+                        mSmartReplyController.stopSending(entry)
                     }
-                } else {
-                    // Mark smart replies as sent whenever a notification is updated by the app,
-                    // otherwise the smart replies are never marked as sent.
-                    mSmartReplyController.stopSending(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
+                // We're removing the notification, the smart reply controller can forget about it.
+                // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear
+                // it.
+                mSmartReplyController.stopSending(entry)
+
+                // When we know the entry will not be lifetime extended, clean up the remote input
+                // view
+                // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
+                if (reason == REASON_CANCEL || reason == REASON_CLICK) {
+                    mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
                 }
             }
         }
 
-        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-            if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
-            // We're removing the notification, the smart reply controller can forget about it.
-            // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
-            mSmartReplyController.stopSending(entry)
-
-            // When we know the entry will not be lifetime extended, clean up the remote input view
-            // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
-            if (reason == REASON_CANCEL || reason == REASON_CLICK) {
-                mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
-            }
-        }
-    }
-
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) }
     }
@@ -183,22 +189,25 @@
         // view it is already canceled, so we'll need to cancel it on the apps behalf
         // now that a reply has been sent. However, delay so that the app has time to posts an
         // update in the mean time, and to give another lifetime extender time to pick it up.
-        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
-                REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+            entry.key,
+            REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+        )
     }
 
     private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) {
         if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})")
         val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply)
-        mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                "Adding smart reply spinner for sent")
+        mNotifUpdater.onInternalNotificationUpdate(newSbn, "Adding smart reply spinner for sent")
 
         // If we're extending for remote input being active, then from the apps point of
         // view it is already canceled, so we'll need to cancel it on the apps behalf
         // now that a reply has been sent. However, delay so that the app has time to posts an
         // update in the mean time, and to give another lifetime extender time to pick it up.
-        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
-                REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+            entry.key,
+            REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+        )
     }
 
     override fun onPanelCollapsed() {
@@ -208,19 +217,25 @@
     override fun isNotificationKeptForRemoteInputHistory(key: String) =
         if (!lifetimeExtensionRefactor()) {
             mRemoteInputHistoryExtender.isExtending(key) ||
-                    mSmartReplyHistoryExtender.isExtending(key)
+                mSmartReplyHistoryExtender.isExtending(key)
         } else false
 
     override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
         if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
         if (!lifetimeExtensionRefactor()) {
-            mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
-                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
-            mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
-                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+            mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(
+                entry.key,
+                REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+            )
+            mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(
+                entry.key,
+                REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+            )
         }
-        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
-                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+            entry.key,
+            REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+        )
     }
 
     override fun setRemoteInputController(remoteInputController: RemoteInputController) {
@@ -229,32 +244,36 @@
 
     @VisibleForTesting
     inner class RemoteInputHistoryExtender :
-            SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
+        SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
 
         override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
-                mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
+            mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
 
         override fun onStartedLifetimeExtension(entry: NotificationEntry) {
             val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
             entry.onRemoteInputInserted()
-            mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                    "Extending lifetime of notification with remote input")
+            mNotifUpdater.onInternalNotificationUpdate(
+                newSbn,
+                "Extending lifetime of notification with remote input",
+            )
             // TODO: Check if the entry was removed due perhaps to an inflation exception?
         }
     }
 
     @VisibleForTesting
     inner class SmartReplyHistoryExtender :
-            SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
+        SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
 
         override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
-                mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
+            mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
 
         override fun onStartedLifetimeExtension(entry: NotificationEntry) {
             val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
             mSmartReplyController.stopSending(entry)
-            mNotifUpdater.onInternalNotificationUpdate(newSbn,
-                    "Extending lifetime of notification with smart reply")
+            mNotifUpdater.onInternalNotificationUpdate(
+                newSbn,
+                "Extending lifetime of notification with smart reply",
+            )
             // TODO: Check if the entry was removed due perhaps to an inflation exception?
         }
 
@@ -266,9 +285,9 @@
 
     @VisibleForTesting
     inner class RemoteInputActiveExtender :
-            SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
+        SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
 
         override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
-                mNotificationRemoteInputManager.isRemoteInputActive(entry)
+            mNotificationRemoteInputManager.isRemoteInputActive(entry)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 6d0148a..41419f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -41,7 +41,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.kotlin.BooleanFlowOperators;
@@ -170,7 +170,7 @@
                     if (entry == null) {
                         return false;
                     }
-                    boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+                    boolean isTopUnseen = NotificationMinimalism.isEnabled()
                             && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
                                 || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
                     if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 2956432..1babe47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.util.printSection
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -57,25 +57,25 @@
 
     /** Set the entry that is identified as the top ongoing notification. */
     fun setTopOngoingNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topOngoingNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top ongoing notification. */
     fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
         else
             entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
 
     /** Set the entry that is identified as the top unseen notification. */
     fun setTopUnseenNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topUnseenNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top unseen notification. */
     fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
         else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
 
     fun dump(pw: IndentingPrintWriter) =
@@ -120,4 +120,29 @@
             // only track the most recent emission, if events are happening faster than they can be
             // consumed
             .conflate()
+
+    fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> =
+        secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+                    default = 1,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
index 06f3db5..f1fc275 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
@@ -14,39 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.shared
+package com.android.systemui.statusbar.notification.emptyshade.shared
 
-import android.os.SystemProperties
-import com.android.systemui.Flags
+import android.app.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the minimalism prototype flag state. */
+/** Helper for reading or using the modes_ui_empty_shade flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object ModesEmptyShadeFix {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_MODES_UI_EMPTY_SHADE
 
     /** A token used for dependency declaration */
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the heads-up cycling animation enabled */
+    /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationMinimalismPrototype()
-
-    /**
-     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
-     * not grouped, but this property read allows that behavior to be disabled.
-     */
-    val ungroupTopUnseen: Boolean
-        get() =
-            if (isUnexpectedlyInLegacyMode()) false
-            else
-                SystemProperties.getBoolean(
-                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                    false
-                )
+        get() = Flags.modesUiEmptyShade()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
@@ -59,6 +46,14 @@
 
     /**
      * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is not enabled to ensure that the refactor author catches issues in testing.
+     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
      * the flag is enabled to ensure that the refactor author catches issues in testing.
      */
     @JvmStatic
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
index 850e944..73477da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.View;
@@ -29,26 +30,71 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 
-public class EmptyShadeView extends StackScrollerDecorView {
+import kotlin.Unit;
+
+import java.util.Objects;
+
+public class EmptyShadeView extends StackScrollerDecorView implements LaunchableView {
 
     private TextView mEmptyText;
     private TextView mEmptyFooterText;
 
-    private @StringRes int mText = R.string.empty_shade_text;
+    private @StringRes int mTextId = R.string.empty_shade_text;
+    private String mTextString;
 
-    private @DrawableRes int mFooterIcon = R.drawable.ic_friction_lock_closed;
-    private @StringRes int mFooterText = R.string.unlock_to_see_notif_text;
+    private @DrawableRes int mFooterIcon;
+    private @StringRes int mFooterText;
+    // This view is initially gone in the xml.
     private @Visibility int mFooterVisibility = View.GONE;
     private int mSize;
 
+    private LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(this,
+            visibility -> {
+                super.setVisibility(visibility);
+                return Unit.INSTANCE;
+            });
+
     public EmptyShadeView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mSize = getResources().getDimensionPixelSize(
                 R.dimen.notifications_unseen_footer_icon_size);
+        if (ModesEmptyShadeFix.isEnabled()) {
+            mTextString = getContext().getString(R.string.empty_shade_text);
+        } else {
+            // These will be set by the binder when appropriate if ModesEmptyShadeFix is on.
+            mFooterIcon = R.drawable.ic_friction_lock_closed;
+            mFooterText = R.string.unlock_to_see_notif_text;
+        }
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        mLaunchableViewDelegate.setVisibility(visibility);
+    }
+
+    @Override
+    public void setShouldBlockVisibilityChanges(boolean block) {
+        /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+        mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
+    }
+
+    @Override
+    public void onActivityLaunchAnimationEnd() {
+        /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+    }
+
+    @Override
+    @NonNull
+    public Rect getPaddingForLaunchAnimation() {
+        /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+        return new Rect();
     }
 
     @Override
@@ -56,7 +102,11 @@
         super.onConfigurationChanged(newConfig);
         mSize = getResources().getDimensionPixelSize(
                 R.dimen.notifications_unseen_footer_icon_size);
-        mEmptyText.setText(mText);
+        if (ModesEmptyShadeFix.isEnabled()) {
+            mEmptyText.setText(mTextString);
+        } else {
+            mEmptyText.setText(mTextId);
+        }
         mEmptyFooterText.setVisibility(mFooterVisibility);
         setFooterText(mFooterText);
         setFooterIcon(mFooterIcon);
@@ -72,25 +122,45 @@
         return findViewById(R.id.no_notifications_footer);
     }
 
+    /** Update view colors. */
     public void setTextColors(@ColorInt int onSurface, @ColorInt int onSurfaceVariant) {
         mEmptyText.setTextColor(onSurfaceVariant);
         mEmptyFooterText.setTextColor(onSurface);
         mEmptyFooterText.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface));
     }
 
+    /** Set the resource ID for the main text shown by the view. */
     public void setText(@StringRes int text) {
-        mText = text;
-        mEmptyText.setText(mText);
+        ModesEmptyShadeFix.assertInLegacyMode();
+        mTextId = text;
+        mEmptyText.setText(mTextId);
     }
 
+    /** Set the string for the main text shown by the view. */
+    public void setText(String text) {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || Objects.equals(mTextString, text)) {
+            return;
+        }
+        mTextString = text;
+        mEmptyText.setText(text);
+    }
+
+    /** Visibility for the footer (the additional icon+text shown below the main text). */
     public void setFooterVisibility(@Visibility int visibility) {
+        if (ModesEmptyShadeFix.isEnabled() && mFooterVisibility == visibility) {
+            return; // nothing to change
+        }
         mFooterVisibility = visibility;
         setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
                 /* animate = */false,
                 /* onAnimationEnded = */ null);
     }
 
+    /** Text resource ID for the footer (the additional icon+text shown below the main text). */
     public void setFooterText(@StringRes int text) {
+        if (ModesEmptyShadeFix.isEnabled() && mFooterText == text) {
+            return; // nothing to change
+        }
         mFooterText = text;
         if (text != 0) {
             mEmptyFooterText.setText(mFooterText);
@@ -99,7 +169,11 @@
         }
     }
 
+    /** Icon resource ID for the footer (the additional icon+text shown below the main text). */
     public void setFooterIcon(@DrawableRes int icon) {
+        if (ModesEmptyShadeFix.isEnabled() && mFooterIcon == icon) {
+            return; // nothing to change
+        }
         mFooterIcon = icon;
         Drawable drawable;
         if (icon == 0) {
@@ -111,18 +185,24 @@
         mEmptyFooterText.setCompoundDrawablesRelative(drawable, null, null, null);
     }
 
+    /** Get resource ID for main text. */
     @StringRes
     public int getTextResource() {
-        return mText;
+        ModesEmptyShadeFix.assertInLegacyMode();
+        return mTextId;
     }
 
+    /** Get resource ID for footer text. */
     @StringRes
     public int getFooterTextResource() {
+        ModesEmptyShadeFix.assertInLegacyMode();
         return mFooterText;
     }
 
+    /** Get resource ID for footer icon. */
     @DrawableRes
     public int getFooterIconResource() {
+        ModesEmptyShadeFix.assertInLegacyMode();
         return mFooterIcon;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
new file mode 100644
index 0000000..7f1b043
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.notification.emptyshade.ui.viewbinder
+
+import android.view.View
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+object EmptyShadeViewBinder {
+    suspend fun bind(
+        view: EmptyShadeView,
+        viewModel: EmptyShadeViewModel,
+        notificationActivityStarter: NotificationActivityStarter,
+    ) = coroutineScope {
+        launch { viewModel.text.collect { view.setText(it) } }
+
+        launch {
+            viewModel.onClick.collect { settingsIntent ->
+                val onClickListener = { view: View ->
+                    notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+                }
+                view.setOnClickListener(onClickListener)
+            }
+        }
+
+        launch { bindFooter(view, viewModel) }
+    }
+
+    private suspend fun bindFooter(view: EmptyShadeView, viewModel: EmptyShadeViewModel) =
+        coroutineScope {
+            // Bind the resource IDs
+            view.setFooterText(viewModel.footer.messageId)
+            view.setFooterIcon(viewModel.footer.iconId)
+
+            launch {
+                viewModel.footer.isVisible.collect { visible ->
+                    view.setFooterVisibility(if (visible) View.VISIBLE else View.GONE)
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
new file mode 100644
index 0000000..8c8f200
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.notification.emptyshade.ui.viewmodel
+
+import android.content.Context
+import android.icu.text.MessageFormat
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.res.R
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel for the empty shade (aka the "No notifications" text shown when there are no
+ * notifications.
+ */
+class EmptyShadeViewModel
+@AssistedInject
+constructor(
+    private val context: Context,
+    zenModeInteractor: ZenModeInteractor,
+    seenNotificationsInteractor: SeenNotificationsInteractor,
+    notificationSettingsInteractor: NotificationSettingsInteractor,
+    dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+    val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
+                "areNotificationsHiddenInShade"
+            )
+        }
+    }
+
+    val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+            MutableStateFlow(false)
+        } else {
+            seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
+                "hasFilteredOutSeenNotifications"
+            )
+        }
+    }
+
+    val text: Flow<String> by lazy {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+            flowOf(context.getString(R.string.empty_shade_text))
+        } else {
+            // Note: Flag modes_ui_empty_shade includes two pieces: refactoring the empty shade to
+            // recommended architecture, and making it so it reacts to changes for the new Modes.
+            // The former does not depend on the modes flags being on, but the latter does.
+            if (ModesUi.isEnabled) {
+                zenModeInteractor.modesHidingNotifications.map { modes ->
+                    // Create a string that is either "No notifications" if no modes are filtering
+                    // them out, or something like "Notifications paused by SomeMode" otherwise.
+                    val msgFormat =
+                        MessageFormat(
+                            context.getString(R.string.modes_suppressing_shade_text),
+                            Locale.getDefault(),
+                        )
+                    val count = modes.count()
+                    val args: MutableMap<String, Any> = HashMap()
+                    args["count"] = count
+                    if (count >= 1) {
+                        args["mode"] = modes[0].name
+                    }
+                    msgFormat.format(args)
+                }
+            } else {
+                areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
+                    if (areNotificationsHiddenInShade) {
+                        context.getString(R.string.dnd_suppressing_shade_text)
+                    } else {
+                        context.getString(R.string.empty_shade_text)
+                    }
+                }
+            }
+        }
+    }
+
+    val footer: FooterMessageViewModel by lazy {
+        ModesEmptyShadeFix.assertInNewMode()
+        FooterMessageViewModel(
+            messageId = R.string.unlock_to_see_notif_text,
+            iconId = R.drawable.ic_friction_lock_closed,
+            isVisible = hasFilteredOutSeenNotifications,
+        )
+    }
+
+    val onClick: Flow<SettingsIntent> by lazy {
+        ModesEmptyShadeFix.assertInNewMode()
+        combine(
+            zenModeInteractor.modesHidingNotifications,
+            notificationSettingsInteractor.isNotificationHistoryEnabled,
+        ) { modes, isNotificationHistoryEnabled ->
+            if (modes.isNotEmpty()) {
+                if (modes.size == 1) {
+                    SettingsIntent.forModeSettings(modes[0].id)
+                } else {
+                    SettingsIntent.forModesSettings()
+                }
+            } else {
+                if (isNotificationHistoryEnabled) {
+                    SettingsIntent.forNotificationHistory()
+                } else {
+                    SettingsIntent.forNotificationSettings()
+                }
+            }
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): EmptyShadeViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 920541d..22bec5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -19,6 +19,8 @@
 import android.view.View
 import androidx.lifecycle.lifecycleScope
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
 import com.android.systemui.util.ui.isAnimating
@@ -36,6 +38,7 @@
         clearAllNotifications: View.OnClickListener,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ): DisposableHandle {
         return footer.repeatWhenAttached {
             lifecycleScope.launch {
@@ -45,6 +48,7 @@
                     clearAllNotifications,
                     launchNotificationSettings,
                     launchNotificationHistory,
+                    notificationActivityStarter,
                 )
             }
         }
@@ -56,6 +60,7 @@
         clearAllNotifications: View.OnClickListener,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ) = coroutineScope {
         launch { bindClearAllButton(footer, viewModel, clearAllNotifications) }
         launch {
@@ -64,6 +69,7 @@
                 viewModel,
                 launchNotificationSettings,
                 launchNotificationHistory,
+                notificationActivityStarter,
             )
         }
         launch { bindMessage(footer, viewModel) }
@@ -113,13 +119,23 @@
         viewModel: FooterViewModel,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ) = coroutineScope {
         launch {
-            viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
-                if (shouldLaunchHistory) {
-                    footer.setManageButtonClickListener(launchNotificationHistory)
-                } else {
-                    footer.setManageButtonClickListener(launchNotificationSettings)
+            if (ModesEmptyShadeFix.isEnabled) {
+                viewModel.manageOrHistoryButtonClick.collect { settingsIntent ->
+                    val onClickListener = { view: View ->
+                        notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+                    }
+                    footer.setManageButtonClickListener(onClickListener)
+                }
+            } else {
+                viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
+                    if (shouldLaunchHistory) {
+                        footer.setManageButtonClickListener(launchNotificationHistory)
+                    } else {
+                        footer.setManageButtonClickListener(launchNotificationSettings)
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 90fb728..a3f4cd2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -16,12 +16,17 @@
 
 package com.android.systemui.statusbar.notification.footer.ui.viewmodel
 
+import android.content.Intent
+import android.provider.Settings
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.util.kotlin.sample
@@ -80,7 +85,7 @@
                         combine(
                                 shadeInteractor.isShadeFullyExpanded,
                                 shadeInteractor.isShadeTouchable,
-                                ::Pair
+                                ::Pair,
                             )
                             .onStart { emit(Pair(false, false)) }
                     ) { clearAllButtonVisible, (isShadeFullyExpanded, animationsEnabled) ->
@@ -93,8 +98,28 @@
     val manageButtonShouldLaunchHistory =
         notificationSettingsInteractor.isNotificationHistoryEnabled
 
+    // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel.
+    val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+            flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS)))
+        } else {
+            notificationSettingsInteractor.isNotificationHistoryEnabled.map {
+                isNotificationHistoryEnabled ->
+                if (isNotificationHistoryEnabled) {
+                    SettingsIntent.forNotificationHistory(
+                        cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+                    )
+                } else {
+                    SettingsIntent.forNotificationSettings(
+                        cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+                    )
+                }
+            }
+        }
+    }
+
     private val manageOrHistoryButtonText: Flow<Int> =
-        manageButtonShouldLaunchHistory.map { shouldLaunchHistory ->
+        notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory ->
             if (shouldLaunchHistory) R.string.manage_notifications_history_text
             else R.string.manage_notifications_text
         }
@@ -128,7 +153,7 @@
                     activeNotificationsInteractor.get(),
                     notificationSettingsInteractor.get(),
                     seenNotificationsInteractor.get(),
-                    shadeInteractor.get()
+                    shadeInteractor.get(),
                 )
             )
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 291dc13..cd228e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -72,14 +72,24 @@
     }
 
     /**
+     * See {@link #setVisible(boolean, boolean, Consumer)}.
+     */
+    public void setVisible(boolean visible, boolean animate) {
+        setVisible(visible, animate, null /* onAnimationEnded */);
+    }
+
+    /**
      * Make this view visible. If {@code false} is passed, the view will fade out its content
      * and set the view Visibility to GONE. If only the content should be changed,
      * {@link #setContentVisibleAnimated(boolean)} can be used.
      *
      * @param visible True if the contents should be visible.
      * @param animate True if we should fade to new visibility.
+     * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+     *                         parameter that represents whether the animation was cancelled.
      */
-    public void setVisible(boolean visible, boolean animate) {
+    public void setVisible(boolean visible, boolean animate,
+            Consumer<Boolean> onAnimationEnded) {
         if (mIsVisible != visible) {
             mIsVisible = visible;
             if (animate) {
@@ -90,10 +100,10 @@
                 } else {
                     setWillBeGone(true);
                 }
-                setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
+                setContentVisible(visible, true /* animate */, onAnimationEnded);
             } else {
                 setVisibility(visible ? VISIBLE : GONE);
-                setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
+                setContentVisible(visible, false /* animate */, onAnimationEnded);
                 setWillBeGone(false);
                 notifyHeightChanged(false /* needsAnimation */);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
index 06f3db5..70bb272 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
@@ -17,23 +17,23 @@
 package com.android.systemui.statusbar.notification.shared
 
 import android.os.SystemProperties
-import com.android.systemui.Flags
+import com.android.server.notification.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the minimalism prototype flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object NotificationMinimalism {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM
 
     /** A token used for dependency declaration */
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the heads-up cycling animation enabled */
+    /** Is the notification minimalism enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationMinimalismPrototype()
+        get() = Flags.notificationMinimalism()
 
     /**
      * The prototype will (by default) use a promoter to ensure that the top unseen notification is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 1431b28..129d4ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -88,6 +88,8 @@
     private ExpandableView mLastVisibleBackgroundChild;
     private float mCurrentScrollVelocity;
     private int mStatusBarState;
+    private boolean mShowingStackOnLockscreen;
+    private float mLockscreenStackFadeInProgress;
     private float mExpandingVelocity;
     private boolean mPanelTracking;
     private boolean mExpansionChanging;
@@ -222,6 +224,7 @@
      * @param isSwipingUp Whether we are swiping up.
      */
     public void setSwipingUp(boolean isSwipingUp) {
+        SceneContainerFlag.assertInLegacyMode();
         if (!isSwipingUp && mIsSwipingUp) {
             // Just stopped swiping up.
             mIsFlingRequiredAfterLockScreenSwipeUp = true;
@@ -240,6 +243,7 @@
      * @param isFlinging Whether we are flinging the shade open or closed.
      */
     public void setFlinging(boolean isFlinging) {
+        SceneContainerFlag.assertInLegacyMode();
         if (isOnKeyguard() && !isFlinging && mIsFlinging) {
             // Just stopped flinging.
             mIsFlingRequiredAfterLockScreenSwipeUp = false;
@@ -624,6 +628,26 @@
         return mStatusBarState == StatusBarState.KEYGUARD;
     }
 
+    public boolean isShowingStackOnLockscreen() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return false;
+        return mShowingStackOnLockscreen;
+    }
+
+    public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mShowingStackOnLockscreen = showingStackOnLockscreen;
+    }
+
+    public float getLockscreenStackFadeInProgress() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        return mLockscreenStackFadeInProgress;
+    }
+
+    public void setLockscreenStackFadeInProgress(float lockscreenStackFadeInProgress) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mLockscreenStackFadeInProgress = lockscreenStackFadeInProgress;
+    }
+
     public void setStatusBarState(int statusBarState) {
         if (mStatusBarState != StatusBarState.KEYGUARD) {
             mIsFlingRequiredAfterLockScreenSwipeUp = false;
@@ -695,6 +719,7 @@
      * @return Whether we need to do a fling down after swiping up on lockscreen.
      */
     public boolean isFlingingAfterSwipeUpOnLockscreen() {
+        SceneContainerFlag.assertInLegacyMode();
         return mIsFlinging && mIsFlingRequiredAfterLockScreenSwipeUp;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index cd3516d..b466bf0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -104,6 +104,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
@@ -567,6 +568,7 @@
     private boolean mHasFilteredOutSeenNotifications;
     @Nullable private SplitShadeStateController mSplitShadeStateController = null;
     private boolean mIsSmallLandscapeLockscreenEnabled = false;
+    private boolean mSuppressHeightUpdates;
 
     /** Pass splitShadeStateController to view and update split shade */
     public void passSplitShadeStateController(SplitShadeStateController splitShadeStateController) {
@@ -686,7 +688,9 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        inflateEmptyShadeView();
+        if (!ModesEmptyShadeFix.isEnabled()) {
+            inflateEmptyShadeView();
+        }
         if (!FooterViewRefactor.isEnabled()) {
             inflateFooterView();
         }
@@ -729,7 +733,9 @@
             inflateFooterView();
             updateFooter();
         }
-        inflateEmptyShadeView();
+        if (!ModesEmptyShadeFix.isEnabled()) {
+            inflateEmptyShadeView();
+        }
         mSectionsManager.reinflateViews();
     }
 
@@ -1453,9 +1459,13 @@
      * 2) Swiping up on lockscreen or flinging down after swipe up
      */
     private boolean shouldSkipHeightUpdate() {
-        return mAmbientState.isOnKeyguard()
-                && (mAmbientState.isSwipingUp()
-                || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+        if (SceneContainerFlag.isEnabled()) {
+            return mSuppressHeightUpdates;
+        } else {
+            return mAmbientState.isOnKeyguard()
+                    && (mAmbientState.isSwipingUp()
+                    || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+        }
     }
 
     /**
@@ -4835,6 +4845,8 @@
     /** Trigger an update for the empty shade resources and visibility. */
     public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
             boolean hasFilteredOutSeenNotifications) {
+        ModesEmptyShadeFix.assertInLegacyMode();
+
         mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
 
         if (areNotificationsHiddenInShade) {
@@ -4853,6 +4865,8 @@
             @StringRes int newTextRes,
             @StringRes int newFooterTextRes,
             @DrawableRes int newFooterIconRes) {
+        ModesEmptyShadeFix.assertInLegacyMode();
+
         int oldTextRes = mEmptyShadeView.getTextResource();
         if (oldTextRes != newTextRes) {
             mEmptyShadeView.setText(newTextRes);
@@ -4874,6 +4888,9 @@
 
     public boolean isEmptyShadeViewVisible() {
         SceneContainerFlag.assertInLegacyMode();
+        if (mEmptyShadeView == null) {
+            return false;
+        }
         return mEmptyShadeView.isVisible();
     }
 
@@ -5330,6 +5347,19 @@
         updateDismissBehavior();
     }
 
+    @Override
+    public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mAmbientState.setShowingStackOnLockscreen(showingStackOnLockscreen);
+    }
+
+    @Override
+    public void setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mAmbientState.setLockscreenStackFadeInProgress(alphaForLockscreenFadeIn);
+        requestChildrenUpdate();
+    }
+
     void setUpcomingStatusBarState(int upcomingStatusBarState) {
         FooterViewRefactor.assertInLegacyMode();
         mUpcomingStatusBarState = upcomingStatusBarState;
@@ -5361,7 +5391,7 @@
 
     public float getOpeningHeight() {
         SceneContainerFlag.assertInLegacyMode();
-        if (mEmptyShadeView.getVisibility() == GONE) {
+        if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
             return getMinExpansionHeight();
         } else {
             return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
@@ -5374,6 +5404,7 @@
     }
 
     public void setPanelFlinging(boolean flinging) {
+        SceneContainerFlag.assertInLegacyMode();
         mAmbientState.setFlinging(flinging);
         if (!flinging) {
             // re-calculate the stack height which was frozen while flinging
@@ -5381,6 +5412,12 @@
         }
     }
 
+    @Override
+    public void suppressHeightUpdates(boolean suppress) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mSuppressHeightUpdates = suppress;
+    }
+
     public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
         mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
     }
@@ -5710,6 +5747,8 @@
     }
 
     private void inflateEmptyShadeView() {
+        ModesEmptyShadeFix.assertInLegacyMode();
+
         EmptyShadeView oldView = mEmptyShadeView;
         EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
                 R.layout.status_bar_no_notifications, this, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index dad6894..7b02d0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -380,7 +380,7 @@
             new StatusBarStateController.StateListener() {
                 @Override
                 public void onStatePreChange(int oldState, int newState) {
-                    if (oldState == StatusBarState.SHADE_LOCKED
+                    if (!SceneContainerFlag.isEnabled() && oldState == StatusBarState.SHADE_LOCKED
                             && newState == KEYGUARD) {
                         mView.requestAnimateEverything();
                     }
@@ -1439,6 +1439,7 @@
     }
 
     public void setPanelFlinging(boolean flinging) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setPanelFlinging(flinging);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 06222fd..3bc5495 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.Compile
 import com.android.systemui.util.children
@@ -74,7 +74,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalismPrototype.isEnabled
+        get() = NotificationMinimalism.isEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -406,7 +406,7 @@
     fun updateResources() {
         maxKeyguardNotifications =
             infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
-        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
+        maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9c0fd0e..e0b0ccd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -148,12 +148,18 @@
             if (isHunGoingToShade) {
                 // Keep 100% opacity for heads up notification going to shade.
                 viewState.setAlpha(1f);
-            } else if (ambientState.isOnKeyguard()) {
+            } else if ((!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard())
+                    || ambientState.isShowingStackOnLockscreen()) {
                 // Adjust alpha for wakeup to lockscreen.
                 if (view.isHeadsUpState()) {
                     // Pulsing HUN should be visible on AOD and stay visible during 
                     // AOD=>lockscreen transition
                     viewState.setAlpha(1f - ambientState.getHideAmount());
+                } else if (SceneContainerFlag.isEnabled()) {
+                    // Take into account scene container-specific Lockscreen fade-in progress
+                    float fadeAlpha = ambientState.getLockscreenStackFadeInProgress();
+                    float dozeAlpha = 1f - ambientState.getDozeAmount();
+                    viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha));
                 } else {
                     // Normal notifications are hidden on AOD and should fade in during 
                     // AOD=>lockscreen transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
index f6722a4..c0f1a56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
@@ -31,6 +31,9 @@
     /** The alpha of the shade in order to show brightness. */
     val alphaForBrightnessMirror = MutableStateFlow(1f)
 
+    /** The alpha of the Notification Stack for lockscreen fade-in */
+    val alphaForLockscreenFadeIn = MutableStateFlow(0f)
+
     /**
      * The bounds of the notification shade scrim / container in the current scene.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 756cd87..32e092b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -56,10 +56,9 @@
 
     /** The rounding of the notification stack. */
     val shadeScrimRounding: Flow<ShadeScrimRounding> =
-        combine(
-                shadeInteractor.shadeMode,
-                isExpandingFromHeadsUp,
-            ) { shadeMode, isExpandingFromHeadsUp ->
+        combine(shadeInteractor.shadeMode, isExpandingFromHeadsUp) {
+                shadeMode,
+                isExpandingFromHeadsUp ->
                 ShadeScrimRounding(
                     isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
                     isBottomRounded = shadeMode != ShadeMode.Single,
@@ -71,6 +70,10 @@
     val alphaForBrightnessMirror: StateFlow<Float> =
         placeholderRepository.alphaForBrightnessMirror.asStateFlow()
 
+    /** The alpha of the Notification Stack for lockscreen fade-in */
+    val alphaForLockscreenFadeIn: StateFlow<Float> =
+        placeholderRepository.alphaForLockscreenFadeIn.asStateFlow()
+
     /** The height of the keyguard's available space bounds */
     val constrainedAvailableSpace: StateFlow<Int> =
         placeholderRepository.constrainedAvailableSpace.asStateFlow()
@@ -99,7 +102,7 @@
     val shouldCloseGuts: Flow<Boolean> =
         combine(
             sceneInteractor.isSceneContainerUserInputOngoing,
-            viewHeightRepository.isCurrentGestureInGuts
+            viewHeightRepository.isCurrentGestureInGuts,
         ) { isUserInputOngoing, isCurrentGestureInGuts ->
             isUserInputOngoing && !isCurrentGestureInGuts
         }
@@ -109,6 +112,11 @@
         placeholderRepository.alphaForBrightnessMirror.value = alpha
     }
 
+    /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+    fun setAlphaForLockscreenFadeIn(alpha: Float) {
+        placeholderRepository.alphaForLockscreenFadeIn.value = alpha
+    }
+
     /** Sets the position of the notification stack in the current scene. */
     fun setShadeScrimBounds(bounds: ShadeScrimBounds?) {
         check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 41c0293..dbe81c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -89,6 +89,12 @@
     /** sets the current QS expand fraction */
     fun setQsExpandFraction(expandFraction: Float)
 
+    /** set whether we are idle on the lockscreen scene */
+    fun setShowingStackOnLockscreen(showingStackOnLockscreen: Boolean)
+
+    /** set the alpha from 0-1f of stack fade-in on lockscreen */
+    fun setAlphaForLockscreenFadeIn(alphaForLockscreenFadeIn: Float)
+
     /** Sets whether the view is displayed in doze mode. */
     fun setDozing(dozing: Boolean)
 
@@ -118,4 +124,7 @@
 
     /** @see addHeadsUpHeightChangedListener */
     fun removeHeadsUpHeightChangedListener(runnable: Runnable)
+
+    /** Sets whether updates to the stack are are suppressed. */
+    fun suppressHeightUpdates(suppress: Boolean)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index dc9615c..ebae235 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import android.view.LayoutInflater
+import android.view.View
 import androidx.lifecycle.lifecycleScope
 import com.android.app.tracing.TraceUtils.traceAsync
 import com.android.internal.logging.MetricsLogger
@@ -25,6 +26,7 @@
 import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,10 @@
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
 import com.android.systemui.statusbar.notification.dagger.SilentHeader
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
@@ -49,6 +55,7 @@
 import com.android.systemui.util.kotlin.awaitCancellationThenDispose
 import com.android.systemui.util.kotlin.getOrNull
 import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
 import java.util.Optional
 import javax.inject.Inject
@@ -84,7 +91,7 @@
 
     fun bindWhileAttached(
         view: NotificationStackScrollLayout,
-        viewController: NotificationStackScrollLayoutController
+        viewController: NotificationStackScrollLayoutController,
     ) {
         val shelf =
             LayoutInflater.from(view.context)
@@ -103,7 +110,13 @@
                     val hasNonClearableSilentNotifications: StateFlow<Boolean> =
                         viewModel.hasNonClearableSilentNotifications.stateIn(this)
                     launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
-                    launch { bindEmptyShade(view) }
+                    launch {
+                        if (ModesEmptyShadeFix.isEnabled) {
+                            reinflateAndBindEmptyShade(view)
+                        } else {
+                            bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
+                        }
+                    }
                     launch {
                         bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
                     }
@@ -121,17 +134,12 @@
     }
 
     private suspend fun bindShelf(shelf: NotificationShelf) {
-        NotificationShelfViewBinder.bind(
-            shelf,
-            viewModel.shelf,
-            falsingManager,
-            nicBinder,
-        )
+        NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder)
     }
 
     private suspend fun reinflateAndBindFooter(
         parentView: NotificationStackScrollLayout,
-        hasNonClearableSilentNotifications: StateFlow<Boolean>
+        hasNonClearableSilentNotifications: StateFlow<Boolean>,
     ) {
         viewModel.footer.getOrNull()?.let { footerViewModel ->
             // The footer needs to be re-inflated every time the theme or the font size changes.
@@ -149,7 +157,7 @@
                             footerView,
                             footerViewModel,
                             parentView,
-                            hasNonClearableSilentNotifications
+                            hasNonClearableSilentNotifications,
                         )
                     }
                 }
@@ -163,13 +171,13 @@
         footerView: FooterView,
         footerViewModel: FooterViewModel,
         parentView: NotificationStackScrollLayout,
-        hasNonClearableSilentNotifications: StateFlow<Boolean>
+        hasNonClearableSilentNotifications: StateFlow<Boolean>,
     ): Unit = coroutineScope {
         val disposableHandle =
             FooterViewBinder.bindWhileAttached(
                 footerView,
                 footerViewModel,
-                clearAllNotifications = {
+                {
                     clearAllNotifications(
                         parentView,
                         // Hide the silent section header (if present) if there will be
@@ -177,16 +185,9 @@
                         hideSilentSection = !hasNonClearableSilentNotifications.value,
                     )
                 },
-                launchNotificationSettings = { view ->
-                    notificationActivityStarter
-                        .get()
-                        .startHistoryIntent(view, /* showHistory= */ false)
-                },
-                launchNotificationHistory = { view ->
-                    notificationActivityStarter
-                        .get()
-                        .startHistoryIntent(view, /* showHistory= */ true)
-                },
+                launchNotificationSettings,
+                launchNotificationHistory,
+                notificationActivityStarter.get(),
             )
         if (SceneContainerFlag.isEnabled) {
             launch {
@@ -194,7 +195,9 @@
                     footerView.setVisible(
                         /* visible = */ animatedVisibility.value,
                         /* animate = */ animatedVisibility.isAnimating,
-                    )
+                    ) {
+                        animatedVisibility.stopAnimating()
+                    }
                 }
             }
         } else {
@@ -211,20 +214,70 @@
         disposableHandle.awaitCancellationThenDispose()
     }
 
-    private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+    private val launchNotificationSettings: (View) -> Unit = { view: View ->
+        notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false)
+    }
+
+    private val launchNotificationHistory: (View) -> Unit = { view ->
+        notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true)
+    }
+
+    private suspend fun reinflateAndBindEmptyShade(parentView: NotificationStackScrollLayout) {
+        ModesEmptyShadeFix.assertInNewMode()
+        // The empty shade needs to be re-inflated every time the theme or the font size
+        // changes.
+        configuration
+            .inflateLayout<EmptyShadeView>(
+                R.layout.status_bar_no_notifications,
+                parentView,
+                attachToRoot = false,
+            )
+            .flowOn(backgroundDispatcher)
+            .collectLatest { emptyShadeView: EmptyShadeView ->
+                traceAsync("bind EmptyShadeView") {
+                    parentView.setEmptyShadeView(emptyShadeView)
+                    bindEmptyShade(emptyShadeView, viewModel.emptyShadeViewFactory.create())
+                }
+            }
+    }
+
+    private suspend fun bindEmptyShadeLegacy(
+        emptyShadeViewModel: EmptyShadeViewModel,
+        parentView: NotificationStackScrollLayout,
+    ) {
+        ModesEmptyShadeFix.assertInLegacyMode()
         combine(
                 viewModel.shouldShowEmptyShadeView,
-                viewModel.areNotificationsHiddenInShade,
-                viewModel.hasFilteredOutSeenNotifications,
-                ::Triple
+                emptyShadeViewModel.areNotificationsHiddenInShade,
+                emptyShadeViewModel.hasFilteredOutSeenNotifications,
+                ::Triple,
             )
             .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
-                parentView.updateEmptyShadeView(
-                    shouldShow,
-                    areNotifsHidden,
-                    hasFilteredNotifs,
+                parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs)
+            }
+    }
+
+    private suspend fun bindEmptyShade(
+        emptyShadeView: EmptyShadeView,
+        emptyShadeViewModel: EmptyShadeViewModel,
+    ): Unit = coroutineScope {
+        ModesEmptyShadeFix.assertInNewMode()
+        launch {
+            emptyShadeView.repeatWhenAttachedToWindow {
+                EmptyShadeViewBinder.bind(
+                    emptyShadeView,
+                    emptyShadeViewModel,
+                    notificationActivityStarter.get(),
                 )
             }
+        }
+        launch {
+            viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow ->
+                emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) {
+                    shouldShow.stopAnimating()
+                }
+            }
+        }
     }
 
     private suspend fun bindSilentHeaderClickListener(
@@ -261,7 +314,7 @@
     private fun clearSilentNotifications(
         view: NotificationStackScrollLayout,
         closeShade: Boolean,
-        hideSilentSection: Boolean
+        hideSilentSection: Boolean,
     ) {
         view.clearSilentNotifications(closeShade, hideSilentSection)
     }
@@ -270,11 +323,7 @@
         if (NotificationsLiveDataStoreRefactor.isEnabled) {
             viewModel.logger.getOrNull()?.let { viewModel ->
                 loggerOptional.getOrNull()?.let { logger ->
-                    NotificationStatsLoggerBinder.bindLogger(
-                        view,
-                        logger,
-                        viewModel,
-                    )
+                    NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 2e37dea..87d70ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -88,6 +88,14 @@
                 viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
             }
             launch { viewModel.qsExpandFraction.collect { view.setQsExpandFraction(it) } }
+            launch {
+                viewModel.isShowingStackOnLockscreen.collect {
+                    view.setShowingStackOnLockscreen(it)
+                }
+            }
+            launch {
+                viewModel.alphaForLockscreenFadeIn.collect { view.setAlphaForLockscreenFadeIn(it) }
+            }
             launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
             launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
             launch {
@@ -103,6 +111,7 @@
             launch {
                 viewModel.shouldCloseGuts.filter { it }.collect { view.closeGutsOnSceneTouch() }
             }
+            launch { viewModel.suppressHeightUpdates.collect { view.suppressHeightUpdates(it) } }
 
             launchAndDispose {
                 view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4e2a46d..935e2a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -23,14 +23,14 @@
 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
@@ -48,22 +48,24 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
-/** ViewModel for the list of notifications. */
+/**
+ * ViewModel for the list of notifications, including child elements like the Clear all/Manage
+ * button at the bottom (the footer) and the "No notifications" text (the empty shade).
+ */
 class NotificationListViewModel
 @Inject
 constructor(
     val shelf: NotificationShelfViewModel,
     val hideListViewModel: HideListViewModel,
     val footer: Optional<FooterViewModel>,
+    val emptyShadeViewFactory: EmptyShadeViewModel.Factory,
     val logger: Optional<NotificationLoggerViewModel>,
     activeNotificationsInteractor: ActiveNotificationsInteractor,
     notificationStackInteractor: NotificationStackInteractor,
     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
     remoteInputInteractor: RemoteInputInteractor,
-    seenNotificationsInteractor: SeenNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
     userSetupInteractor: UserSetupInteractor,
-    zenModeInteractor: ZenModeInteractor,
     @Background bgDispatcher: CoroutineDispatcher,
     dumpManager: DumpManager,
 ) : FlowDumperImpl(dumpManager) {
@@ -90,6 +92,7 @@
     }
 
     val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+        ModesEmptyShadeFix.assertInLegacyMode()
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
             flowOf(false)
         } else {
@@ -114,6 +117,45 @@
         }
     }
 
+    val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+            flowOf(AnimatedValue.NotAnimating(false))
+        } else {
+            combine(
+                    activeNotificationsInteractor.areAnyNotificationsPresent,
+                    shadeInteractor.isQsFullscreen,
+                    notificationStackInteractor.isShowingOnLockscreen,
+                ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
+                    when {
+                        hasNotifications -> false
+                        isQsFullScreen -> false
+                        // Do not show the empty shade if the lockscreen is visible (including AOD
+                        // b/228790482 and bouncer b/267060171), except if the shade is opened on
+                        // top.
+                        isShowingOnLockscreen -> false
+                        else -> true
+                    }
+                }
+                .distinctUntilChanged()
+                .sample(
+                    // TODO(b/322167853): This check is currently duplicated in FooterViewModel
+                    //  but instead it should be a field in ShadeAnimationInteractor.
+                    combine(
+                            shadeInteractor.isShadeFullyExpanded,
+                            shadeInteractor.isShadeTouchable,
+                            ::Pair,
+                        )
+                        .onStart { emit(Pair(false, false)) }
+                ) { visible, (isShadeFullyExpanded, animationsEnabled) ->
+                    val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+                    AnimatableEvent(visible, shouldAnimate)
+                }
+                .toAnimatedValueFlow()
+                .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated")
+                .flowOn(bgDispatcher)
+        }
+    }
+
     /**
      * Whether the footer should not be visible for the user, even if it's present in the list (as
      * per [shouldIncludeFooterView] below).
@@ -154,7 +196,7 @@
                     userSetupInteractor.isUserSetUp,
                     notificationStackInteractor.isShowingOnLockscreen,
                     shadeInteractor.isQsFullscreen,
-                    remoteInputInteractor.isRemoteInputActive
+                    remoteInputInteractor.isRemoteInputActive,
                 ) {
                     hasNotifications,
                     isUserSetUp,
@@ -193,7 +235,7 @@
                     combine(
                             shadeInteractor.isShadeFullyExpanded,
                             shadeInteractor.isShadeTouchable,
-                            ::Pair
+                            ::Pair,
                         )
                         .onStart { emit(Pair(false, false)) }
                 ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -263,7 +305,7 @@
                     combine(
                             shadeInteractor.isShadeFullyExpanded,
                             shadeInteractor.isShadeTouchable,
-                            ::Pair
+                            ::Pair,
                         )
                         .onStart { emit(Pair(false, false)) }
                 ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -283,29 +325,7 @@
     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
         DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
         DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
-        APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
-    }
-
-    // TODO(b/308591475): This should be tracked separately by the empty shade.
-    val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
-                "areNotificationsHiddenInShade"
-            )
-        }
-    }
-
-    // TODO(b/308591475): This should be tracked separately by the empty shade.
-    val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting(
-                "hasFilteredOutSeenNotifications"
-            )
-        }
+        APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
     }
 
     val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
new file mode 100644
index 0000000..84aa997
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.util.kotlin.ActivatableFlowDumper
+import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class NotificationLockscreenScrimViewModel
+@AssistedInject
+constructor(
+    dumpManager: DumpManager,
+    shadeInteractor: ShadeInteractor,
+    private val stackAppearanceInteractor: NotificationStackAppearanceInteractor,
+) :
+    ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
+    ExclusiveActivatable() {
+
+    val shadeMode = shadeInteractor.shadeMode
+
+    /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+    fun setAlphaForLockscreenFadeIn(alpha: Float) {
+        stackAppearanceInteractor.setAlphaForLockscreenFadeIn(alpha)
+    }
+
+    override suspend fun onActivated(): Nothing {
+        activateFlowDumper()
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): NotificationLockscreenScrimViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 5b2e02d..c9eaec7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.ObservableTransitionState.Idle
 import com.android.compose.animation.scene.ObservableTransitionState.Transition
 import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene
@@ -48,6 +49,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@@ -82,8 +84,13 @@
     private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean {
         // The lockscreen stack is visible during all transitions away from the lockscreen, so keep
         // the stack expanded until those transitions finish.
-        return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) ||
-            change.isBetween({ it == Scenes.Lockscreen }, { true })
+        return if (change.isFrom({ it == Scenes.Lockscreen }, to = { true })) {
+            true
+        } else if (change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })) {
+            false
+        } else {
+            (expandedInScene(change.fromScene) && expandedInScene(change.toScene))
+        }
     }
 
     private fun expandFractionDuringSceneChange(
@@ -93,7 +100,10 @@
     ): Float {
         return if (fullyExpandedDuringSceneChange(change)) {
             1f
-        } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) {
+        } else if (
+            change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade }) ||
+                change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })
+        ) {
             shadeExpansion
         } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
             // during QS expansion, increase fraction at same rate as scrim alpha,
@@ -121,6 +131,14 @@
         }
     }
 
+    /** Are notification stack height updates suppressed? */
+    val suppressHeightUpdates: Flow<Boolean> =
+        sceneInteractor.transitionState.map { transition: ObservableTransitionState ->
+            transition is Transition &&
+                transition.fromContent == Scenes.Lockscreen &&
+                (transition.toContent == Scenes.Bouncer || transition.toContent == Scenes.Gone)
+        }
+
     /**
      * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
      * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
@@ -178,6 +196,18 @@
             .distinctUntilChanged()
             .dumpWhileCollecting("shouldResetStackTop")
 
+    /** Whether the Notification Stack is visibly on the lockscreen scene. */
+    val isShowingStackOnLockscreen: Flow<Boolean> =
+        sceneInteractor.transitionState
+            .mapNotNull { state ->
+                state.isIdle(Scenes.Lockscreen) ||
+                    state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
+            }
+            .distinctUntilChanged()
+
+    /** The alpha of the Notification Stack for lockscreen fade-in */
+    val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn
+
     private operator fun SceneKey.contains(scene: SceneKey) =
         sceneInteractor.isSceneInFamily(scene, this)
 
@@ -298,3 +328,6 @@
 
 private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean =
     (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
+
+private fun ChangeScene.isFrom(from: (SceneKey) -> Boolean, to: (SceneKey) -> Boolean): Boolean =
+    from(fromScene) && to(toScene)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 57be629..0ad22e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -132,6 +133,7 @@
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
         PrimaryBouncerToLockscreenTransitionViewModel,
@@ -444,6 +446,7 @@
             occludedToAodTransitionViewModel.lockscreenAlpha,
             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
             occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+            offToLockscreenTransitionViewModel.lockscreenAlpha,
             primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
             glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
             lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 0a6e7f5..ee96195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -43,6 +43,7 @@
 import android.util.EventLog;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -74,6 +75,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
@@ -249,7 +251,7 @@
      * Called when a notification is clicked.
      *
      * @param entry notification that was clicked
-     * @param row row for that notification
+     * @param row   row for that notification
      */
     @Override
     public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
@@ -547,8 +549,8 @@
                             (adapter) -> TaskStackBuilder.create(mContext)
                                     .addNextIntentWithParentStack(intent)
                                     .startActivities(getActivityOptions(
-                                            mDisplayId,
-                                            adapter),
+                                                    mDisplayId,
+                                                    adapter),
                                             new UserHandle(UserHandle.getUserId(appUid))));
                 });
                 return true;
@@ -565,6 +567,7 @@
 
     @Override
     public void startHistoryIntent(View view, boolean showHistory) {
+        ModesEmptyShadeFix.assertInLegacyMode();
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
@@ -585,14 +588,14 @@
                             );
                     ActivityTransitionAnimator.Controller animationController =
                             viewController == null ? null
-                                : new StatusBarTransitionAnimatorController(
-                                        viewController,
-                                        mShadeAnimationInteractor,
-                                        mShadeController,
-                                        mNotificationShadeWindowController,
-                                        mCommandQueue,
-                                        mDisplayId,
-                                        true /* isActivityIntent */);
+                                    : new StatusBarTransitionAnimatorController(
+                                            viewController,
+                                            mShadeAnimationInteractor,
+                                            mShadeController,
+                                            mNotificationShadeWindowController,
+                                            mCommandQueue,
+                                            mDisplayId,
+                                            true /* isActivityIntent */);
 
                     mActivityTransitionAnimator.startIntentWithAnimation(
                             animationController, animate, intent.getPackage(),
@@ -612,6 +615,51 @@
                 false /* afterKeyguardGone */);
     }
 
+    @Override
+    public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) {
+        boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
+        ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
+            @Override
+            public boolean onDismiss() {
+                AsyncTask.execute(() -> {
+                    TaskStackBuilder tsb = TaskStackBuilder.create(mContext);
+                    for (Intent intent : intentInfo.getBackStack()) {
+                        tsb.addNextIntent(intent);
+                    }
+                    tsb.addNextIntent(intentInfo.getTargetIntent());
+
+                    ActivityTransitionAnimator.Controller viewController =
+                            ActivityTransitionAnimator.Controller.fromView(view,
+                                    intentInfo.getCujType());
+                    ActivityTransitionAnimator.Controller animationController =
+                            viewController == null ? null
+                                    : new StatusBarTransitionAnimatorController(
+                                            viewController,
+                                            mShadeAnimationInteractor,
+                                            mShadeController,
+                                            mNotificationShadeWindowController,
+                                            mCommandQueue,
+                                            mDisplayId,
+                                            true /* isActivityIntent */);
+
+                    mActivityTransitionAnimator.startIntentWithAnimation(
+                            animationController, animate, intentInfo.getTargetIntent().getPackage(),
+                            (adapter) -> tsb.startActivities(
+                                    getActivityOptions(mDisplayId, adapter),
+                                    mUserTracker.getUserHandle()));
+                });
+                return true;
+            }
+
+            @Override
+            public boolean willRunAnimationOnKeyguard() {
+                return animate;
+            }
+        };
+        mActivityStarter.dismissKeyguardThenExecute(onDismissAction, null,
+                false /* afterKeyguardGone */);
+    }
+
     private void removeHunAfterClick(ExpandableNotificationRow row) {
         String key = row.getEntry().getSbn().getKey();
         if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt
new file mode 100644
index 0000000..f8958e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.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.pipeline.shared.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+
+sealed interface InternetTileIconModel {
+    data class ResourceId(val resId: Int) : InternetTileIconModel
+
+    data class Cellular(val level: Int) : InternetTileIconModel
+
+    data class Satellite(val resourceIcon: Icon.Resource) : InternetTileIconModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index caf09a3..674cbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,8 +46,6 @@
 
     private val tag = "AvalancheController"
     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
-    var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
-
     var enableAtRuntime = true
         set(value) {
             if (!value) {
@@ -118,43 +116,32 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "Runnable NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
             return
         }
         if (!isEnabled) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "NOT ENABLED, run runnable. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
+                    "NOT ENABLED, run runnable")
             runnable.run()
             return
         }
         if (entry == null) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "Entry NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
             return
         }
         if (debug) {
             debugRunnableLabelMap[runnable] = caller
         }
-        var stateAfter = ""
+        var outcome = ""
         if (isShowing(entry)) {
+            outcome = "update showing"
             runnable.run()
-            stateAfter = "update showing"
-
         } else if (entry in nextMap) {
+            outcome = "update next"
             nextMap[entry]?.add(runnable)
-            stateAfter = "update next"
-
         } else if (headsUpEntryShowing == null) {
+            outcome = "show now"
             showNow(entry, arrayListOf(runnable))
-            stateAfter = "show now"
-
         } else {
             // Clean up invalid state when entry is in list but not map and vice versa
             if (entry in nextMap) nextMap.remove(entry)
@@ -175,8 +162,8 @@
                 )
             }
         }
-        stateAfter += getStateStr()
-        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
+        outcome += getStateStr()
+        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
     }
 
     @VisibleForTesting
@@ -194,40 +181,32 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled, key,
-                "Runnable NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
             return
         }
         if (!isEnabled) {
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+                    "NOT ENABLED, run runnable")
             runnable.run()
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled = false, key,
-                "NOT ENABLED, run runnable. ${getStateStr()}"
-            )
             return
         }
         if (entry == null) {
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+                    "Entry NULL, run runnable")
             runnable.run()
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled = true, key,
-                "Entry NULL, run runnable. ${getStateStr()}"
-            )
             return
         }
-        val stateAfter: String
+        val outcome: String
         if (entry in nextMap) {
+            outcome = "remove from next"
             if (entry in nextMap) nextMap.remove(entry)
             if (entry in nextList) nextList.remove(entry)
             uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
-            stateAfter = "remove from next. ${getStateStr()}"
-
         } else if (entry in debugDropSet) {
+            outcome = "remove from dropset"
             debugDropSet.remove(entry)
-            stateAfter = "remove from dropset. ${getStateStr()}"
-
         } else if (isShowing(entry)) {
+            outcome = "remove showing"
             previousHunKey = getKey(headsUpEntryShowing)
             // Show the next HUN before removing this one, so that we don't tell listeners
             // onHeadsUpPinnedModeChanged, which causes
@@ -235,13 +214,11 @@
             // HUN is animating out, resulting in a flicker.
             showNext()
             runnable.run()
-            stateAfter = "remove showing. ${getStateStr()}"
-
         } else {
+            outcome = "run runnable for untracked shown"
             runnable.run()
-            stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
         }
-        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
+        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
     }
 
     /**
@@ -423,14 +400,12 @@
     }
 
     private fun getStateStr(): String {
-        return "\nAvalancheController:" +
+        return "\navalanche state:" +
                 "\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
                 "\n\tprevious: [$previousHunKey]" +
                 "\n\tnext list: $nextListStr" +
                 "\n\tnext map: $nextMapStr" +
-                "\n\tdropped: $dropSetStr" +
-                "\nBHUM.mHeadsUpEntryMap: " +
-                baseEntryMapStr()
+                "\n\tdropped: $dropSetStr"
     }
 
     private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 30524a5..f37393a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,7 +116,6 @@
         mAccessibilityMgr = accessibilityManagerWrapper;
         mUiEventLogger = uiEventLogger;
         mAvalancheController = avalancheController;
-        mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
         Resources resources = context.getResources();
         mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
                 ? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -590,18 +589,6 @@
         dumpInternal(pw, args);
     }
 
-    private String getEntryMapStr() {
-        if (mHeadsUpEntryMap.isEmpty()) {
-            return "EMPTY";
-        }
-        StringBuilder entryMapStr = new StringBuilder();
-        for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
-            entryMapStr.append("\n\t").append(
-                    entry.mEntry == null ? "null" : entry.mEntry.getKey());
-        }
-        return entryMapStr.toString();
-    }
-
     protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -1005,6 +992,7 @@
          * Clear any pending removal runnables.
          */
         public void cancelAutoRemovalCallbacks(@Nullable String reason) {
+            mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
             Runnable runnable = () -> {
                 final boolean removed = cancelAutoRemovalCallbackInternal();
 
@@ -1013,7 +1001,6 @@
                 }
             };
             if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
-                mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
                 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
             } else {
                 // Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 41112cb..600270c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        stateAfter: String
+        outcome: String
     ) {
         buffer.log(
             TAG,
@@ -60,7 +60,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = stateAfter
+                str3 = outcome
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        stateAfter: String
+        outcome: String
     ) {
         buffer.log(
             TAG,
@@ -79,7 +79,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = stateAfter
+                str3 = outcome
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
+            { "request: cancel auto remove of $str1 reason: $str2" }
         )
     }
 
@@ -148,7 +148,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "$str2 => cancel auto remove: $str1" }
+            { "cancel auto remove of $str1 reason: $str2" }
         )
     }
 
@@ -161,7 +161,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+            { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -174,7 +174,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+            { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -216,12 +216,12 @@
                 str1 = logKey(key)
                 str2 = reason
             },
-            { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
+            { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
         )
     }
 
     fun logNotificationActuallyRemoved(entry: NotificationEntry) {
-        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
+        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
     }
 
     fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
+            { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -246,7 +246,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
+            { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -281,7 +281,7 @@
                 bool1 = isPinned
                 str2 = reason
             },
-            { "$str2 => setEntryPinned[$bool1]: $str1" }
+            { "$str2 => set entry pinned $str1 pinned: $bool1" }
         )
     }
 
@@ -290,7 +290,7 @@
             TAG,
             INFO,
             { bool1 = hasPinnedNotification },
-            { "hasPinnedNotification[$bool1]" }
+            { "has pinned notification changed to $bool1" }
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index ba45942..daba109 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -20,6 +20,7 @@
 import android.provider.Settings
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
 import android.util.Log
 import androidx.concurrent.futures.await
 import com.android.settingslib.notification.data.repository.ZenModeRepository
@@ -29,6 +30,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.modes.shared.ModesUi
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 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.model.ActiveZenModes
@@ -39,6 +41,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
@@ -54,8 +57,8 @@
     private val notificationSettingsRepository: NotificationSettingsRepository,
     @Background private val bgDispatcher: CoroutineDispatcher,
     private val iconLoader: ZenIconLoader,
-    private val deviceProvisioningRepository: DeviceProvisioningRepository,
-    private val userSetupRepository: UserSetupRepository,
+    deviceProvisioningRepository: DeviceProvisioningRepository,
+    userSetupRepository: UserSetupRepository,
 ) {
     val isZenAvailable: Flow<Boolean> =
         combine(
@@ -126,6 +129,25 @@
     val mainActiveMode: Flow<ZenModeInfo?> =
         activeModes.map { a -> a.mainMode }.distinctUntilChanged()
 
+    val modesHidingNotifications: Flow<List<ZenMode>> by lazy {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) {
+            flowOf(listOf())
+        } else {
+            modes
+                .map { modes ->
+                    modes.filter { mode ->
+                        mode.isActive &&
+                            !mode.policy.isVisualEffectAllowed(
+                                /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST,
+                                /* defaultVal = */ true,
+                            )
+                    }
+                }
+                .flowOn(bgDispatcher)
+                .distinctUntilChanged()
+        }
+    }
+
     suspend fun getModeIcon(mode: ZenMode): ZenIcon {
         return iconLoader.getIcon(context, mode).await()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index e1dcc52..a1d5cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -19,17 +19,21 @@
 import android.content.Intent
 import android.provider.Settings
 import android.util.Log
+import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.PlatformButton
 import com.android.compose.PlatformOutlinedButton
+import com.android.compose.theme.PlatformTheme
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
@@ -74,7 +78,7 @@
             currentDialog?.dismiss()
         }
 
-        currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) }
+        currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) }
         currentDialog
             ?.lifecycle
             ?.addObserver(
@@ -91,28 +95,40 @@
 
     @Composable
     private fun ModesDialogContent(dialog: SystemUIDialog) {
-        AlertDialogContent(
-            modifier = Modifier.semantics {
-                testTagsAsResourceId = true
-            },
-            title = {
-                Text(
-                    modifier = Modifier.testTag("modes_title"),
-                    text = stringResource(R.string.zen_modes_dialog_title)
-                )
-            },
-            content = { ModeTileGrid(viewModel.get()) },
-            neutralButton = {
-                PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
-                    Text(stringResource(R.string.zen_modes_dialog_settings))
-                }
-            },
-            positiveButton = {
-                PlatformButton(onClick = { dialog.dismiss() }) {
-                    Text(stringResource(R.string.zen_modes_dialog_done))
-                }
-            },
-        )
+        // TODO(b/369376884): The composable does correctly update when the theme changes
+        //  while the dialog is open, but the background (which we don't control here)
+        //  doesn't, which causes us to show things like white text on a white background.
+        //  as a workaround, we remember the original theme and keep it on recomposition.
+        val isCurrentlyInDarkTheme = isSystemInDarkTheme()
+        val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
+        PlatformTheme(isDarkTheme = cachedDarkTheme) {
+            AlertDialogContent(
+                modifier =
+                    Modifier.semantics {
+                        testTagsAsResourceId = true
+                        paneTitle = dialog.context.getString(
+                            R.string.accessibility_desc_quick_settings
+                        )
+                    },
+                title = {
+                    Text(
+                        modifier = Modifier.testTag("modes_title"),
+                        text = stringResource(R.string.zen_modes_dialog_title),
+                    )
+                },
+                content = { ModeTileGrid(viewModel.get()) },
+                neutralButton = {
+                    PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
+                        Text(stringResource(R.string.zen_modes_dialog_settings))
+                    }
+                },
+                positiveButton = {
+                    PlatformButton(onClick = { dialog.dismiss() }) {
+                        Text(stringResource(R.string.zen_modes_dialog_done))
+                    }
+                },
+            )
+        }
     }
 
     @VisibleForTesting
@@ -128,8 +144,8 @@
         }
         activityStarter.startActivity(
             ZEN_MODE_SETTINGS_INTENT,
-            true /* dismissShade */,
-            animationController
+            /* dismissShade= */ true,
+            animationController,
         )
     }
 
@@ -163,7 +179,7 @@
             Log.w(
                 TAG,
                 "Cannot launch from dialog, the dialog is not present. " +
-                    "Will launch activity without animating."
+                    "Will launch activity without animating.",
             )
         }
 
@@ -172,11 +188,7 @@
         if (animationController == null) {
             currentDialog?.dismiss()
         }
-        activityStarter.startActivity(
-            intent,
-            true, /* dismissShade */
-            animationController,
-        )
+        activityStarter.startActivity(intent, /* dismissShade= */ true, animationController)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index d03b2e7..e1f7bd5 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -29,6 +29,7 @@
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
+import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger
 import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen
@@ -45,6 +46,7 @@
 constructor(
     private val viewModelFactory: TouchpadTutorialViewModel.Factory,
     private val logger: InputDeviceTutorialLogger,
+    private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger,
 ) : ComponentActivity() {
 
     private val vm by viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory })
@@ -57,6 +59,7 @@
         }
         // required to handle 3+ fingers on touchpad
         window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        metricsLogger.logPeripheralTutorialLaunchedFromSettings()
         logger.logOpenTutorial(TutorialContext.TOUCHPAD_TUTORIAL)
     }
 
@@ -85,7 +88,7 @@
                 onBackTutorialClicked = { vm.goTo(BACK_GESTURE) },
                 onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) },
                 onRecentAppsTutorialClicked = { vm.goTo(RECENT_APPS_GESTURE) },
-                onDoneButtonClicked = closeTutorial
+                onDoneButtonClicked = closeTutorial,
             )
         BACK_GESTURE ->
             BackGestureTutorialScreen(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index aa8c6b7..e160ff1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
@@ -643,6 +644,46 @@
         environment.verifyInputSessionDispose();
     }
 
+    @Test
+    public void testSessionPopAfterDestroy() {
+        final TouchHandler touchHandler = createTouchHandler();
+
+        final Environment environment = new Environment(Stream.of(touchHandler)
+                .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+
+        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(initialEvent);
+
+        // Ensure session started
+        final InputChannelCompat.InputEventListener eventListener =
+                registerInputEventListener(touchHandler);
+
+        // First event will be missed since we register after the execution loop,
+        final InputEvent event = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(event);
+        verify(eventListener).onInputEvent(eq(event));
+
+        final ArgumentCaptor<TouchHandler.TouchSession> touchSessionArgumentCaptor =
+                ArgumentCaptor.forClass(TouchHandler.TouchSession.class);
+
+        verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture());
+
+        environment.updateLifecycle(Lifecycle.State.DESTROYED);
+
+        // Check to make sure the input session is now disposed.
+        environment.verifyInputSessionDispose();
+
+        clearInvocations(environment.mInputFactory);
+
+        // Pop the session
+        touchSessionArgumentCaptor.getValue().pop();
+
+        environment.executeAll();
+
+        // Ensure no input sessions were created due to the session reset.
+        verifyNoMoreInteractions(environment.mInputFactory);
+    }
+
 
     @Test
     public void testPilfering() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 71afa62..5cc6454 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -62,6 +62,7 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
 import com.android.systemui.scene.shared.logger.sceneLogger
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
@@ -70,6 +71,7 @@
 import com.android.systemui.scene.ui.composable.SceneContainer
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
 import kotlin.time.Duration.Companion.seconds
@@ -133,6 +135,8 @@
                 shadeInteractor = kosmos.shadeInteractor,
                 splitEdgeDetector = kosmos.splitEdgeDetector,
                 logger = kosmos.sceneLogger,
+                gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+                displayId = kosmos.displayTracker.defaultDisplayId,
                 motionEventHandlerReceiver = {},
             )
             .apply { setTransitionState(transitionState) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b0810a9..6608542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -100,6 +100,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -199,6 +200,7 @@
     private @Mock ShadeWindowLogger mShadeWindowLogger;
     private @Mock SelectedUserInteractor mSelectedUserInteractor;
     private @Mock KeyguardInteractor mKeyguardInteractor;
+    private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
     private @Captor ArgumentCaptor<KeyguardStateController.Callback>
             mKeyguardStateControllerCallback;
     private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -1294,6 +1296,7 @@
                 () -> mock(WindowManagerLockscreenVisibilityManager.class),
                 mSelectedUserInteractor,
                 mKeyguardInteractor,
+                mKeyguardTransitionBootInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
index 8a5af09..ad5eeab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -65,7 +65,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     private val goneToLs =
@@ -75,7 +75,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     @Before
@@ -84,7 +84,8 @@
         kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
         testScope.launch {
             kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff(
-                KeyguardState.LOCKSCREEN
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
             )
         }
     }
@@ -105,11 +106,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertTransition(
@@ -142,11 +139,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -191,7 +184,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
             sceneTransitions.value = lsToGone
@@ -205,11 +198,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -257,11 +246,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -297,7 +282,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -330,11 +315,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             val stepM3 = allSteps[allSteps.size - 3]
@@ -393,7 +374,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -466,7 +447,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -523,7 +504,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -577,11 +558,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -589,7 +566,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -641,11 +618,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -653,7 +626,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -702,11 +675,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -714,7 +683,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -736,7 +705,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -777,7 +746,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -799,7 +768,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
             allSteps[allSteps.size - 3]
 
@@ -858,7 +827,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -880,7 +849,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -959,7 +928,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -977,7 +946,7 @@
                     from = KeyguardState.AOD,
                     to = KeyguardState.DOZING,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -995,7 +964,7 @@
                     from = KeyguardState.DOZING,
                     to = KeyguardState.OCCLUDED,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -1017,7 +986,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1077,7 +1046,7 @@
                         from = KeyguardState.LOCKSCREEN,
                         to = KeyguardState.AOD,
                         animator = null,
-                        modeOnCanceled = TransitionModeOnCanceled.RESET
+                        modeOnCanceled = TransitionModeOnCanceled.RESET,
                     )
                 )
 
@@ -1092,7 +1061,7 @@
             kosmos.realKeyguardTransitionRepository.updateTransition(
                 ktfUuid!!,
                 1f,
-                TransitionState.FINISHED
+                TransitionState.FINISHED,
             )
 
             assertTransition(
@@ -1110,7 +1079,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1171,7 +1140,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1235,7 +1204,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1291,7 +1260,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1308,7 +1277,7 @@
         from: KeyguardState? = null,
         to: KeyguardState? = null,
         state: TransitionState? = null,
-        progress: Float? = null
+        progress: Float? = null,
     ) {
         if (from != null) assertThat(step.from).isEqualTo(from)
         if (to != null) assertThat(step.to).isEqualTo(to)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index d32d8cc..fb376ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -1890,7 +1890,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1911,7 +1911,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1928,7 +1928,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1939,7 +1939,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1983,7 +1983,7 @@
             backgroundExecutor.runAllReady()
             foregroundExecutor.runAllReady()
 
-            stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+            onStateUpdated(PACKAGE_NAME, state)
 
             verify(listener)
                 .onMediaDataLoaded(
@@ -2008,7 +2008,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2518,4 +2518,10 @@
                 eq(false),
             )
     }
+
+    private fun onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        backgroundExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 90af932..7d364bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -1967,7 +1967,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1988,7 +1988,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2005,7 +2005,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2016,7 +2016,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2059,7 +2059,7 @@
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
 
-        stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+        testScope.onStateUpdated(PACKAGE_NAME, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2084,7 +2084,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2603,4 +2603,11 @@
                 eq(false),
             )
     }
+
+    /** Helper function to update state and run executors */
+    private fun TestScope.onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        runCurrent()
+        advanceUntilIdle()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 680df15..dcf32a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -137,7 +137,7 @@
             MediaTestUtils.emptyMediaData.copy(
                 app = PACKAGE,
                 packageName = PACKAGE,
-                token = session.sessionToken
+                token = session.sessionToken,
             )
 
         resumeData = mediaData.copy(token = null, active = false, resumption = true)
@@ -237,7 +237,7 @@
         // Assuming we're registered
         testOnMediaDataLoaded_registersPlaybackListener()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -249,7 +249,7 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(0)
@@ -261,7 +261,7 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -435,7 +435,7 @@
 
         // When the playback state changes, and has different actions
         val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
         assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
@@ -448,7 +448,7 @@
             PlaybackState.CustomAction.Builder(
                     "ACTION_1",
                     "custom action 1",
-                    android.R.drawable.ic_media_ff
+                    android.R.drawable.ic_media_ff,
                 )
                 .build()
         val pausedState =
@@ -463,7 +463,7 @@
             PlaybackState.CustomAction.Builder(
                     "ACTION_2",
                     "custom action 2",
-                    android.R.drawable.ic_media_rew
+                    android.R.drawable.ic_media_rew,
                 )
                 .build()
         val pausedStateTwoActions =
@@ -472,7 +472,7 @@
                 .addCustomAction(customOne)
                 .addCustomAction(customTwo)
                 .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+        onPlaybackStateChanged(pausedStateTwoActions)
         assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
@@ -485,7 +485,7 @@
         loadMediaDataWithPlaybackState(stateWithActions)
 
         // When the playback state updates with the same actions
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
+        onPlaybackStateChanged(stateWithActions)
 
         // Then the callback is not invoked again
         verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -512,7 +512,7 @@
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customTwo)
                 .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
+        onPlaybackStateChanged(stateTwo)
 
         // Then the callback is not invoked
         verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -544,7 +544,7 @@
         // When the playback state changes to playing
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
         uiExecutor.runAllReady()
 
         // Then the callback is invoked
@@ -561,7 +561,7 @@
         // When the playback state is updated, but still not playing
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
 
         // Then the callback is not invoked
         verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
@@ -571,7 +571,7 @@
     fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
         // When paused media is loaded
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -597,7 +597,7 @@
         val time = clock.currentTimeMillis()
         clock.setElapsedRealtime(time)
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -706,4 +706,9 @@
         bgExecutor.runAllReady()
         uiExecutor.runAllReady()
     }
+
+    private fun onPlaybackStateChanged(state: PlaybackState) {
+        mediaCallbackCaptor.value.onPlaybackStateChanged(state)
+        bgExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 03667cf..570c640 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -21,19 +21,19 @@
 import android.content.res.Configuration
 import android.database.ContentObserver
 import android.os.LocaleList
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
 import android.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.internal.logging.InstanceId
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.DisableSceneContainer
@@ -71,7 +71,6 @@
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.testKosmos
-import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
@@ -106,6 +105,8 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 private val DATA = MediaTestUtils.emptyMediaData
 
@@ -116,8 +117,8 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testDispatcher = kosmos.unconfinedTestDispatcher
     private val secureSettings = kosmos.unconfinedDispatcherFakeSettings
@@ -129,7 +130,6 @@
     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
     @Mock lateinit var mediaHostState: MediaHostState
     @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
     @Mock lateinit var mediaDataManager: MediaDataManager
     @Mock lateinit var configurationController: ConfigurationController
     @Mock lateinit var falsingManager: FalsingManager
@@ -153,16 +153,33 @@
 
     private val clock = FakeSystemClock()
     private lateinit var bgExecutor: FakeExecutor
+    private lateinit var uiExecutor: FakeExecutor
     private lateinit var mediaCarouselController: MediaCarouselController
 
     private var originalResumeSetting =
         Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.progressionOf(
+                com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND
+            )
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
         bgExecutor = FakeExecutor(clock)
+        uiExecutor = FakeExecutor(clock)
+
         mediaCarouselController =
             MediaCarouselController(
                 applicationScope = kosmos.applicationCoroutineScope,
@@ -173,7 +190,7 @@
                 activityStarter = activityStarter,
                 systemClock = clock,
                 mainDispatcher = kosmos.testDispatcher,
-                executor = executor,
+                uiExecutor = uiExecutor,
                 bgExecutor = bgExecutor,
                 backgroundDispatcher = testDispatcher,
                 mediaManager = mediaDataManager,
@@ -201,10 +218,11 @@
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
         FakeExecutor.exhaustExecutors(bgExecutor)
+        FakeExecutor.exhaustExecutors(uiExecutor)
         verify(globalSettings)
             .registerContentObserverSync(
                 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
-                capture(settingsObserverCaptor)
+                capture(settingsObserverCaptor),
             )
     }
 
@@ -213,7 +231,7 @@
         Settings.Secure.putInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RESUME,
-            originalResumeSetting
+            originalResumeSetting,
         )
     }
 
@@ -227,9 +245,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                4500L
+                4500L,
             )
 
         val playingCast =
@@ -239,9 +257,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedLocal =
@@ -251,9 +269,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                1000L
+                1000L,
             )
 
         val pausedCast =
@@ -263,9 +281,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                2000L
+                2000L,
             )
 
         val playingRcn =
@@ -275,9 +293,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedRcn =
@@ -287,9 +305,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val active =
@@ -299,9 +317,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                250L
+                250L,
             )
 
         val resume1 =
@@ -311,9 +329,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                500L
+                500L,
             )
 
         val resume2 =
@@ -323,9 +341,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                1000L
+                1000L,
             )
 
         val activeMoreRecent =
@@ -336,9 +354,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 2L
+                    lastActive = 2L,
                 ),
-                1000L
+                1000L,
             )
 
         val activeLessRecent =
@@ -349,9 +367,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 1L
+                    lastActive = 1L,
                 ),
-                1000L
+                1000L,
             )
         // Expected ordering for media players:
         // Actively playing local sessions
@@ -370,7 +388,7 @@
                 pausedRcn,
                 active,
                 resume2,
-                resume1
+                resume1,
             )
 
         expected.forEach {
@@ -380,7 +398,7 @@
                 it.second.copy(notificationKey = it.first),
                 panel,
                 clock,
-                isSsReactivated = false
+                isSsReactivated = false,
             )
         }
 
@@ -403,7 +421,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             true,
-            clock
+            clock,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -421,7 +439,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -439,7 +457,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             false,
-            clock
+            clock,
         )
 
         // Then it should be shown at the end of the carousel's active entries
@@ -461,8 +479,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -471,19 +489,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         // paused player order should stays the same in visibleMediaPLayer map.
         // paused player order should be first in mediaPlayer map.
         assertEquals(
             MediaPlayerData.visiblePlayerKeys().elementAt(3),
-            MediaPlayerData.playerKeys().elementAt(0)
+            MediaPlayerData.playerKeys().elementAt(0),
         )
     }
 
@@ -506,7 +525,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             LOCATION_QS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(LOCATION_QS)
@@ -517,7 +536,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_QQS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
@@ -528,7 +547,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_LOCKSCREEN,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -539,7 +558,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
@@ -570,8 +589,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -580,14 +599,15 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
         // adding a media recommendation card.
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
         mediaCarouselController.shouldScrollToKey = true
         // switching between media players.
@@ -598,8 +618,8 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -608,13 +628,14 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -626,7 +647,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
-            false
+            false,
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -635,14 +656,15 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(
             playerIndex,
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         assertEquals(playerIndex, 0)
 
@@ -657,9 +679,10 @@
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
                 resumption = false,
-                packageName = "PACKAGE_NAME"
-            )
+                packageName = "PACKAGE_NAME",
+            ),
         )
+        runAllReady()
         playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(playerIndex, 0)
     }
@@ -704,7 +727,7 @@
             player1.second.copy(notificationKey = player1.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
@@ -717,7 +740,7 @@
             player2.second.copy(notificationKey = player2.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -732,7 +755,7 @@
             player3.second.copy(notificationKey = player3.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -822,7 +845,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then the carousel is updated
@@ -841,7 +864,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
 
         // Then it is added to the carousel with correct state
@@ -886,7 +909,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.GONE,
-                this
+                this,
             )
 
             verify(mediaCarousel).visibility = View.VISIBLE
@@ -932,7 +955,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -961,7 +984,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -1125,12 +1148,14 @@
         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
 
         // This is processed as a user-initiated dismissal
         verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true))
@@ -1148,12 +1173,14 @@
 
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can't be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
         visualStabilityCallback.value.onReorderingAllowed()
 
         // This is processed as a user-initiated dismissal
@@ -1175,8 +1202,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -1185,18 +1212,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         val playersSize = MediaPlayerData.players().size
         reset(pageIndicator)
         function()
+        runAllReady()
 
         assertEquals(playersSize, MediaPlayerData.players().size)
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -1225,4 +1254,11 @@
         )
         runCurrent()
     }
+
+    private fun runAllReady() {
+        if (mediaControlsUmoInflationInBackground()) {
+            bgExecutor.runAllReady()
+            uiExecutor.runAllReady()
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index eea02ee..2f8f45c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -887,6 +888,34 @@
     }
 
     @Test
+    public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+
+        // Adds non DDS subId
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        doReturn(false).when(info).isOpportunistic();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+        // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId()
+        verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+        assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+
+        // Adds non DDS subId again
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        doReturn(false).when(info).isOpportunistic();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+        // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap.
+        verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+        assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+    }
+
+    @Test
     public void getMobileNetworkSummary() {
         mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
         Resources res1 = mock(Resources.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index a1750cd..b1ec740 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
@@ -28,6 +29,7 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.setSceneTransition
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
@@ -38,9 +40,9 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
+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
 
@@ -55,14 +57,9 @@
     private val configurationRepository = kosmos.fakeConfigurationRepository
     private val keyguardRepository = kosmos.fakeKeyguardRepository
     private val sceneInteractor = kosmos.sceneInteractor
-    private val shadeTestUtil = kosmos.shadeTestUtil
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
 
-    private lateinit var underTest: ShadeInteractorSceneContainerImpl
-
-    @Before
-    fun setUp() {
-        underTest = kosmos.shadeInteractorSceneContainerImpl
-    }
+    private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
 
     @Test
     fun qsExpansionWhenInSplitShadeAndQsExpanded() =
@@ -600,14 +597,14 @@
 
     @Test
     @EnableFlags(DualShade.FLAG_NAME)
-    fun expandNotificationShade_dualShadeEnabled_opensOverlay() =
+    fun expandNotificationsShade_dualShade_opensOverlay() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).isEmpty()
 
-            underTest.expandNotificationShade("reason")
+            underTest.expandNotificationsShade("reason")
 
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
@@ -615,14 +612,15 @@
 
     @Test
     @DisableFlags(DualShade.FLAG_NAME)
-    fun expandNotificationShade_dualShadeDisabled_switchesToShadeScene() =
+    fun expandNotificationsShade_singleShade_switchesToShadeScene() =
         testScope.runTest {
+            shadeTestUtil.setSplitShade(false)
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).isEmpty()
 
-            underTest.expandNotificationShade("reason")
+            underTest.expandNotificationsShade("reason")
 
             assertThat(currentScene).isEqualTo(Scenes.Shade)
             assertThat(currentOverlays).isEmpty()
@@ -630,7 +628,7 @@
 
     @Test
     @EnableFlags(DualShade.FLAG_NAME)
-    fun expandNotificationShade_dualShadeEnabledAndQuickSettingsOpen_replacesOverlay() =
+    fun expandNotificationsShade_dualShadeQuickSettingsOpen_replacesOverlay() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
@@ -638,14 +636,14 @@
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade)
 
-            underTest.expandNotificationShade("reason")
+            underTest.expandNotificationsShade("reason")
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
         }
 
     @Test
     @EnableFlags(DualShade.FLAG_NAME)
-    fun expandQuickSettingsShade_dualShadeEnabled_opensOverlay() =
+    fun expandQuickSettingsShade_dualShade_opensOverlay() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
@@ -660,8 +658,9 @@
 
     @Test
     @DisableFlags(DualShade.FLAG_NAME)
-    fun expandQuickSettingsShade_dualShadeDisabled_switchesToQuickSettingsScene() =
+    fun expandQuickSettingsShade_singleShade_switchesToQuickSettingsScene() =
         testScope.runTest {
+            shadeTestUtil.setSplitShade(false)
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
@@ -674,12 +673,28 @@
         }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun expandQuickSettingsShade_splitShade_switchesToShadeScene() =
+        testScope.runTest {
+            shadeTestUtil.setSplitShade(true)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+
+            underTest.expandQuickSettingsShade("reason")
+
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
     @EnableFlags(DualShade.FLAG_NAME)
-    fun expandQuickSettingsShade_dualShadeEnabledAndNotificationsOpen_replacesOverlay() =
+    fun expandQuickSettingsShade_dualShadeNotificationsOpen_replacesOverlay() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-            underTest.expandNotificationShade("reason")
+            underTest.expandNotificationsShade("reason")
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
 
@@ -687,4 +702,141 @@
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
             assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade)
         }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun collapseNotificationsShade_dualShade_hidesOverlay() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            openShade(Overlays.NotificationsShade)
+
+            underTest.collapseNotificationsShade("reason")
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun collapseNotificationsShade_singleShade_switchesToLockscreen() =
+        testScope.runTest {
+            shadeTestUtil.setSplitShade(false)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            sceneInteractor.changeScene(Scenes.Shade, "reason")
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(currentOverlays).isEmpty()
+
+            underTest.collapseNotificationsShade("reason")
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun collapseQuickSettingsShade_dualShade_hidesOverlay() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            openShade(Overlays.QuickSettingsShade)
+
+            underTest.collapseQuickSettingsShade("reason")
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun collapseQuickSettingsShadeNotBypassingShade_singleShade_switchesToShade() =
+        testScope.runTest {
+            shadeTestUtil.setSplitShade(false)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+            assertThat(currentOverlays).isEmpty()
+
+            underTest.collapseQuickSettingsShade(
+                loggingReason = "reason",
+                bypassNotificationsShade = false,
+            )
+
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun collapseQuickSettingsShadeNotBypassingShade_splitShade_switchesToLockscreen() =
+        testScope.runTest {
+            shadeTestUtil.setSplitShade(true)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+            assertThat(currentOverlays).isEmpty()
+
+            underTest.collapseQuickSettingsShade(
+                loggingReason = "reason",
+                bypassNotificationsShade = false,
+            )
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun collapseQuickSettingsShadeBypassingShade_singleShade_switchesToLockscreen() =
+        testScope.runTest {
+            shadeTestUtil.setSplitShade(false)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+            assertThat(currentOverlays).isEmpty()
+
+            underTest.collapseQuickSettingsShade(
+                loggingReason = "reason",
+                bypassNotificationsShade = true,
+            )
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun collapseEitherShade_dualShade_hidesBothOverlays() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            openShade(Overlays.QuickSettingsShade)
+            openShade(Overlays.NotificationsShade)
+            assertThat(currentOverlays)
+                .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)
+
+            underTest.collapseEitherShade("reason")
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    private fun TestScope.openShade(overlay: OverlayKey) {
+        val isAnyExpanded by collectLastValue(underTest.isAnyExpanded)
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+        val initialScene = checkNotNull(currentScene)
+        sceneInteractor.showOverlay(overlay, "reason")
+        kosmos.setSceneTransition(
+            ObservableTransitionState.Idle(initialScene, checkNotNull(currentOverlays))
+        )
+        runCurrent()
+        assertThat(currentScene).isEqualTo(initialScene)
+        assertThat(currentOverlays).contains(overlay)
+        assertThat(isAnyExpanded).isTrue()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 0407fc1..ac73882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -24,7 +24,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import com.android.systemui.util.DeviceConfigProxyFake
 import com.android.systemui.util.Utils
@@ -42,7 +42,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 // this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME)
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     lateinit var manager: NotificationSectionsFeatureManager
     private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
index 07c29a0..0c65c9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -20,7 +20,6 @@
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
@@ -30,15 +29,12 @@
 
 import android.app.Notification.MediaStyle;
 import android.media.session.MediaSession;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.service.notification.NotificationListenerService;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
@@ -158,8 +154,7 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
-    public void inflateMediaNotificationIconsMediaEnabled_old() throws InflationException {
+    public void inflateMediaNotificationIconsMediaEnabled() throws InflationException {
         finishSetupWithMediaFeatureFlagEnabled(true);
 
         mListener.onEntryInit(mMediaEntry);
@@ -187,37 +182,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
-    public void inflateMediaNotificationIconsMediaEnabled_new() throws InflationException {
-        finishSetupWithMediaFeatureFlagEnabled(true);
-
-        mListener.onEntryInit(mMediaEntry);
-        mListener.onEntryAdded(mMediaEntry);
-        verify(mIconManager).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-        clearInvocations(mIconManager);
-
-        mFilter.shouldFilterOut(mMediaEntry, 0);
-        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-
-        mListener.onEntryUpdated(mMediaEntry);
-        verify(mIconManager, never()).createIcons(eq(mMediaEntry));
-        verify(mIconManager).updateIcons(eq(mMediaEntry), /* usingCache = */ eq(false));
-
-        mListener.onEntryRemoved(mMediaEntry, NotificationListenerService.REASON_CANCEL);
-        mListener.onEntryCleanUp(mMediaEntry);
-        clearInvocations(mIconManager);
-
-        mListener.onEntryInit(mMediaEntry);
-        mListener.onEntryAdded(mMediaEntry);
-        verify(mIconManager).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-    }
-
-    @Test
-    @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
-    public void inflationException_old() throws InflationException {
+    public void inflationException() throws InflationException {
         finishSetupWithMediaFeatureFlagEnabled(true);
 
         mListener.onEntryInit(mMediaEntry);
@@ -244,31 +209,6 @@
         verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
     }
 
-    @Test
-    @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
-    public void inflationException_new() throws InflationException {
-        finishSetupWithMediaFeatureFlagEnabled(true);
-
-        doThrow(InflationException.class).when(mIconManager).createIcons(eq(mMediaEntry));
-
-        mListener.onEntryInit(mMediaEntry);
-        mListener.onEntryAdded(mMediaEntry);
-        verify(mIconManager).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-        clearInvocations(mIconManager);
-
-        mListener.onEntryUpdated(mMediaEntry);
-        verify(mIconManager).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-        clearInvocations(mIconManager);
-
-        doNothing().when(mIconManager).createIcons(eq(mMediaEntry));
-
-        mListener.onEntryUpdated(mMediaEntry);
-        verify(mIconManager).createIcons(eq(mMediaEntry));
-        verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-    }
-
     private void finishSetupWithMediaFeatureFlagEnabled(boolean mediaFeatureFlagEnabled) {
         when(mMediaFeatureFlag.getEnabled()).thenReturn(mediaFeatureFlagEnabled);
         mCoordinator = new MediaCoordinator(mMediaFeatureFlag, mStatusBarService, mIconManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index deb3fc1..a3f8452 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -15,8 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.app.Flags.lifetimeExtensionRefactor
 import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
+import android.app.Flags.lifetimeExtensionRefactor
 import android.app.Notification
 import android.app.RemoteInputHistoryItem
 import android.os.Handler
@@ -47,10 +47,10 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations.initMocks
 
 @SmallTest
@@ -78,21 +78,20 @@
     @Before
     fun setUp() {
         initMocks(this)
-        coordinator = RemoteInputCoordinator(
+        coordinator =
+            RemoteInputCoordinator(
                 dumpManager,
                 rebuilder,
                 remoteInputManager,
                 mainHandler,
-                smartReplyController
-        )
+                smartReplyController,
+            )
         `when`(pipeline.addNotificationLifetimeExtender(any())).thenAnswer {
             (it.arguments[0] as NotifLifetimeExtender).setCallback(lifetimeExtensionCallback)
         }
         `when`(pipeline.getInternalNotifUpdater(any())).thenReturn(notifUpdater)
         coordinator.attach(pipeline)
-        listener = withArgCaptor {
-            verify(remoteInputManager).setRemoteInputListener(capture())
-        }
+        listener = withArgCaptor { verify(remoteInputManager).setRemoteInputListener(capture()) }
         entry1 = NotificationEntryBuilder().setId(1).build()
         entry2 = NotificationEntryBuilder().setId(2).build()
         `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -101,13 +100,17 @@
         `when`(rebuilder.rebuildWithExistingReplies(any())).thenReturn(sbn)
     }
 
-    val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender
-    val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
-    val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+    val remoteInputActiveExtender
+        get() = coordinator.mRemoteInputActiveExtender
 
-    val collectionListeners get() = captureMany {
-        verify(pipeline, times(1)).addCollectionListener(capture())
-    }
+    val remoteInputHistoryExtender
+        get() = coordinator.mRemoteInputHistoryExtender
+
+    val smartReplyHistoryExtender
+        get() = coordinator.mSmartReplyHistoryExtender
+
+    val collectionListeners
+        get() = captureMany { verify(pipeline, times(1)).addCollectionListener(capture()) }
 
     @Test
     fun testRemoteInputActive() {
@@ -179,7 +182,8 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testRemoteInputLifetimeExtensionListenerTrigger() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
@@ -187,9 +191,7 @@
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
 
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
     }
@@ -198,16 +200,15 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testSmartReplyLifetimeExtensionListenerTrigger() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                 .build()
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
         verify(smartReplyController, times(1)).stopSending(entry)
@@ -217,25 +218,25 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testRepeatedUpdateTriggersRebuild() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                 .build()
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
-        verify(rebuilder, times(1)).rebuildWithExistingReplies(entry)
+        verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
     }
 
     @Test
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testLifetimeExtensionListenerClearsRemoteInputs() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
@@ -245,9 +246,7 @@
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
 
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         assertThat(entry.remoteInputs).isNull()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index cea8857..7d5278e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -332,7 +332,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
 
             assertThat(showHeadsUpStatusBar).isFalse()
         }
@@ -345,7 +348,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
@@ -359,7 +365,10 @@
 
             // WHEN no pinned rows
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 83ad18b..46f3a6b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ui.isAnimating
@@ -254,6 +255,39 @@
             assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text)
         }
 
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun manageButtonOnClick_whenHistoryDisabled() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.manageOrHistoryButtonClick)
+            runCurrent()
+
+            // WHEN notification history is disabled
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)
+
+            // THEN onClick leads to settings page
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun historyButtonOnClick_whenHistoryEnabled() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.manageOrHistoryButtonClick)
+            runCurrent()
+
+            // WHEN notification history is enabled
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)
+
+            // THEN onClick leads to history page
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY)
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
+        }
+
     @Test
     fun manageButtonVisible_whenMessageVisible() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index e396b56..0598b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -133,7 +133,10 @@
 
             // WHEN HUN displayed on the bypass lock screen
             headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true))
-            keyguardTransitionRepository.emitInitialStepsFromOff(KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
new file mode 100644
index 0000000..cd681a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view
+
+import android.graphics.Region
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+val Kosmos.mockWindowManagerService: IWindowManager by
+    Kosmos.Fixture {
+        mock(IWindowManager::class.java).apply {
+            whenever(registerSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+                val listener = answer.arguments[0] as ISystemGestureExclusionListener
+                val displayId = answer.arguments[1] as Int
+                exclusionListeners.getOrPut(displayId) { mutableListOf() }.add(listener)
+                listener.onSystemGestureExclusionChanged(
+                    displayId,
+                    restrictedRegionByDisplayId[displayId],
+                    null,
+                )
+            }
+
+            whenever(unregisterSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+                val listener = answer.arguments[0] as ISystemGestureExclusionListener
+                val displayId = answer.arguments[1] as Int
+                exclusionListeners[displayId]?.remove(listener)
+            }
+        }
+    }
+
+var Kosmos.windowManagerService: IWindowManager by Kosmos.Fixture { mockWindowManagerService }
+
+private var restrictedRegionByDisplayId = mutableMapOf<Int, Region?>()
+private var exclusionListeners = mutableMapOf<Int, MutableList<ISystemGestureExclusionListener>>()
+
+fun setSystemGestureExclusionRegion(displayId: Int, restrictedRegion: Region?) {
+    restrictedRegionByDisplayId[displayId] = restrictedRegion
+    exclusionListeners[displayId]?.forEach { listener ->
+        listener.onSystemGestureExclusionChanged(displayId, restrictedRegion, null)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a73c184..4d0e603 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -48,9 +48,8 @@
  * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior.
  */
 @SysUISingleton
-class FakeKeyguardTransitionRepository(
-    private val initInLockscreen: Boolean = true,
-) : KeyguardTransitionRepository {
+class FakeKeyguardTransitionRepository(private val initInLockscreen: Boolean = true) :
+    KeyguardTransitionRepository {
     private val _transitions =
         MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
@@ -63,7 +62,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.LOCKSCREEN,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -71,12 +70,7 @@
     init {
         // Seed with a FINISHED transition in OFF, same as the real repository.
         _transitions.tryEmit(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
 
         if (initInLockscreen) {
@@ -173,7 +167,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 0.5f
+                        value = 0.5f,
                     )
             )
             testScheduler.runCurrent()
@@ -184,7 +178,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 1f
+                        value = 1f,
                     )
             )
             testScheduler.runCurrent()
@@ -208,7 +202,7 @@
         this.sendTransitionStep(
             step = step,
             validateStep = validateStep,
-            ownerName = step.ownerName
+            ownerName = step.ownerName,
         )
     }
 
@@ -240,9 +234,9 @@
                 to = to,
                 value = value,
                 transitionState = transitionState,
-                ownerName = ownerName
+                ownerName = ownerName,
             ),
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ) {
         if (step.transitionState == TransitionState.STARTED) {
             _currentTransitionInfo.value =
@@ -273,7 +267,7 @@
     fun sendTransitionStepJava(
         coroutineScope: CoroutineScope,
         step: TransitionStep,
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ): Job {
         return coroutineScope.launch {
             sendTransitionStep(step = step, validateStep = validateStep)
@@ -283,7 +277,7 @@
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
-        validateSteps: Boolean = true
+        validateSteps: Boolean = true,
     ) {
         steps.forEach {
             sendTransitionStep(step = it, validateStep = validateSteps)
@@ -296,7 +290,7 @@
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
         tryEmitInitialStepsFromOff(to)
     }
 
@@ -318,14 +312,14 @@
                 1f,
                 TransitionState.FINISHED,
                 ownerName = "KeyguardTransitionRepository(boot)",
-            ),
+            )
         )
     }
 
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) = Unit
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index e2b283b..2f13ba4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -34,11 +34,11 @@
             transitionInteractor = keyguardTransitionInteractor,
             dismissInteractor = keyguardDismissInteractor,
             applicationScope = testScope.backgroundScope,
-            sceneInteractor = { sceneInteractor },
             deviceUnlockedInteractor = { deviceUnlockedInteractor },
             powerInteractor = powerInteractor,
             alternateBouncerInteractor = alternateBouncerInteractor,
             shadeInteractor = { shadeInteractor },
             keyguardInteractor = { keyguardInteractor },
+            sceneInteractor = { sceneInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
index 0c538ff..ab7ccb3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.os.fakeExecutorHandler
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.keyguardBlueprintViewModel by
@@ -25,5 +26,6 @@
         KeyguardBlueprintViewModel(
             fakeExecutorHandler,
             keyguardBlueprintInteractor,
+            keyguardTransitionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 38626a5..3c87106 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -47,6 +47,8 @@
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         alternateBouncerToLockscreenTransitionViewModel =
             alternateBouncerToLockscreenTransitionViewModel,
+        alternateBouncerToOccludedTransitionViewModel =
+            alternateBouncerToOccludedTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
@@ -69,9 +71,12 @@
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
         lockscreenToPrimaryBouncerTransitionViewModel =
             lockscreenToPrimaryBouncerTransitionViewModel,
+        occludedToAlternateBouncerTransitionViewModel =
+            occludedToAlternateBouncerTransitionViewModel,
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..2acd1b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.occludedToAlternateBouncerTransitionViewModel by Fixture {
+    OccludedToAlternateBouncerTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..5d62a0f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.offToLockscreenTransitionViewModel by Fixture {
+    OffToLockscreenTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
index ff8b478..6540ed6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
@@ -18,11 +18,13 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
 
 val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by
     Kosmos.Fixture {
         QuickSettingsShadeOverlayContentViewModel(
+            shadeInteractor = shadeInteractor,
             sceneInteractor = sceneInteractor,
             shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
             quickSettingsContainerViewModel = quickSettingsContainerViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 8744638..737aaf2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -6,13 +6,16 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.interactor.systemGestureExclusionInteractor
 import com.android.systemui.scene.shared.logger.sceneLogger
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.FakeOverlay
+import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -30,10 +33,7 @@
 val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen }
 
 var Kosmos.overlayKeys by Fixture {
-    listOf(
-        Overlays.NotificationsShade,
-        Overlays.QuickSettingsShade,
-    )
+    listOf(Overlays.NotificationsShade, Overlays.QuickSettingsShade)
 }
 
 val Kosmos.fakeOverlaysByKeys by Fixture { overlayKeys.associateWith { FakeOverlay(it) } }
@@ -74,8 +74,21 @@
             powerInteractor = powerInteractor,
             shadeInteractor = shadeInteractor,
             splitEdgeDetector = splitEdgeDetector,
+            gestureFilterFactory = sceneContainerGestureFilterFactory,
+            displayId = displayTracker.defaultDisplayId,
             motionEventHandlerReceiver = {},
-            logger = sceneLogger
+            logger = sceneLogger,
         )
         .apply { setTransitionState(transitionState) }
 }
+
+val Kosmos.sceneContainerGestureFilterFactory by Fixture {
+    object : SceneContainerGestureFilter.Factory {
+        override fun create(displayId: Int): SceneContainerGestureFilter {
+            return SceneContainerGestureFilter(
+                interactor = systemGestureExclusionInteractor,
+                displayId = displayId,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
new file mode 100644
index 0000000..15ed1b3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.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.scene.data.repository
+
+import android.view.windowManagerService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.systemGestureExclusionRepository by Fixture {
+    SystemGestureExclusionRepository(windowManager = windowManagerService)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
new file mode 100644
index 0000000..3e46c3f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.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.scene.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.data.repository.systemGestureExclusionRepository
+
+val Kosmos.systemGestureExclusionInteractor by Fixture {
+    SystemGestureExclusionInteractor(repository = systemGestureExclusionRepository)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index 6d488d2..60141c6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.shade.data.repository.ShadeRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
@@ -133,7 +134,7 @@
 class ShadeTestUtilLegacyImpl(
     val testScope: TestScope,
     val shadeRepository: FakeShadeRepository,
-    val context: SysuiTestableContext
+    val context: SysuiTestableContext,
 ) : ShadeTestUtilDelegate {
     override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
         shadeRepository.setLegacyShadeExpansion(shadeExpansion)
@@ -191,6 +192,7 @@
 }
 
 /** Sets up shade state for tests when the scene container flag is enabled. */
+@OptIn(ExperimentalCoroutinesApi::class)
 class ShadeTestUtilSceneImpl(
     val testScope: TestScope,
     val sceneInteractor: SceneInteractor,
@@ -269,7 +271,7 @@
         from: SceneKey,
         to: SceneKey,
         progress: Float,
-        isInitiatedByUserInput: Boolean = true
+        isInitiatedByUserInput: Boolean = true,
     ) {
         sceneInteractor.changeScene(from, "test")
         val transitionState =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 9cdd519..718347f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
 
 val Kosmos.notificationsShadeOverlayContentViewModel:
@@ -28,5 +29,6 @@
         shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
         notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
         sceneInteractor = sceneInteractor,
+        shadeInteractor = shadeInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 7eb9f34..f5b856d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.plugins.activityStarter
-import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.privacyChipInteractor
 import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -32,7 +31,6 @@
         ShadeHeaderViewModel(
             context = applicationContext,
             activityStarter = activityStarter,
-            sceneInteractor = sceneInteractor,
             shadeInteractor = shadeInteractor,
             mobileIconsInteractor = mobileIconsInteractor,
             mobileIconsViewModel = mobileIconsViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
index b19e221..3d2bd6c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
@@ -45,3 +45,17 @@
             UserHandle.USER_CURRENT,
         )
     }
+
+var Kosmos.lockScreenNotificationMinimalismSetting: Boolean
+    get() =
+        fakeSettings.getIntForUser(
+            Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            UserHandle.USER_CURRENT,
+        ) == 1
+    set(value) {
+        fakeSettings.putIntForUser(
+            Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            if (value) 1 else 0,
+            UserHandle.USER_CURRENT,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
new file mode 100644
index 0000000..8fdb948
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.notification.emptyshade.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+
+val Kosmos.emptyShadeViewModel by
+    Kosmos.Fixture {
+        EmptyShadeViewModel(
+            applicationContext,
+            zenModeInteractor,
+            seenNotificationsInteractor,
+            notificationSettingsInteractor,
+            dumpManager,
+        )
+    }
+
+val Kosmos.emptyShadeViewModelFactory: EmptyShadeViewModel.Factory by
+    Kosmos.Fixture {
+        object : EmptyShadeViewModel.Factory {
+            override fun create() = emptyShadeViewModel
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index de8b350..c3bc744 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -23,13 +23,12 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
 import java.util.Optional
 
 val Kosmos.notificationListViewModel by Fixture {
@@ -37,15 +36,14 @@
         notificationShelfViewModel,
         hideListViewModel,
         Optional.of(footerViewModel),
+        emptyShadeViewModelFactory,
         Optional.of(notificationListLoggerViewModel),
         activeNotificationsInteractor,
         notificationStackInteractor,
         headsUpNotificationInteractor,
         remoteInputInteractor,
-        seenNotificationsInteractor,
         shadeInteractor,
         userSetupInteractor,
-        zenModeInteractor,
         testDispatcher,
         dumpManager,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index a9e117a..237f7e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.occludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.offToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.kosmos.Kosmos
@@ -85,6 +86,7 @@
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToGoneTransitionViewModel = occludedToGoneTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
             primaryBouncerToLockscreenTransitionViewModel,
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 10e4f38..6b1197a 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -267,25 +267,39 @@
     visibility: ["//frameworks/base"],
 }
 
-cc_library_shared {
-    name: "libravenwood_runtime",
-    host_supported: true,
-
+cc_defaults {
+    name: "ravenwood_jni_defaults",
     cflags: [
         "-Wall",
         "-Werror",
         "-Wno-unused-parameter",
         "-Wthread-safety",
     ],
-
-    srcs: [
-        "runtime-jni/*.cpp",
+    static_libs: [
+        "libnativehelper_jvm",
     ],
+    shared_libs: [
+        "liblog",
+    ],
+}
 
+// We need this as a separate library because we need to overload the
+// sysprop symbols before libbase is loaded into the process
+cc_library_host_shared {
+    name: "libravenwood_sysprop",
+    defaults: ["ravenwood_jni_defaults"],
+    srcs: ["runtime-jni/ravenwood_sysprop.cpp"],
+}
+
+cc_library_host_shared {
+    name: "libravenwood_runtime",
+    defaults: ["ravenwood_jni_defaults"],
+    srcs: [
+        "runtime-jni/ravenwood_runtime.cpp",
+        "runtime-jni/ravenwood_os_constants.cpp",
+    ],
     shared_libs: [
         "libbase",
-        "liblog",
-        "libnativehelper",
         "libutils",
         "libcutils",
     ],
@@ -377,8 +391,10 @@
         "z00-all-updatable-modules-system-stubs",
     ],
     jni_libs: [
-        "libandroid_runtime",
+        // Libraries has to be loaded in the following order
+        "libravenwood_sysprop",
         "libravenwood_runtime",
+        "libandroid_runtime",
     ],
 }
 
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
index e548611..d29b93c 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
@@ -43,6 +43,7 @@
             android.util.Log.class,
             android.os.Parcel.class,
             android.os.Binder.class,
+            android.os.SystemProperties.class,
             android.content.res.ApkAssets.class,
             android.content.res.AssetManager.class,
             android.content.res.StringBlock.class,
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 90bb93d..6e73b2c 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -31,6 +31,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
@@ -38,6 +39,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.internal.os.RuntimeInit;
+import com.android.ravenwood.RavenwoodRuntimeNative;
 import com.android.ravenwood.common.RavenwoodCommonUtils;
 import com.android.ravenwood.common.RavenwoodRuntimeException;
 import com.android.ravenwood.common.SneakyThrow;
@@ -68,6 +70,8 @@
     }
 
     private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+    private static final String RAVENWOOD_NATIVE_SYSPROP_NAME = "ravenwood_sysprop";
+    private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
 
     /**
      * When enabled, attempt to dump all thread stacks just before we hit the
@@ -118,6 +122,7 @@
     }
 
     private static RavenwoodConfig sConfig;
+    private static RavenwoodSystemProperties sProps;
     private static boolean sInitialized = false;
 
     /**
@@ -132,6 +137,14 @@
         // We haven't initialized liblog yet, so directly write to System.out here.
         RavenwoodCommonUtils.log(TAG, "globalInit()");
 
+        // Load libravenwood_sysprop first
+        var libProp = RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_SYSPROP_NAME);
+        System.load(libProp);
+        RavenwoodRuntimeNative.reloadNativeLibrary(libProp);
+
+        // Make sure libravenwood_runtime is loaded.
+        System.load(RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_RUNTIME_NAME));
+
         // Do the basic set up for the android sysprops.
         setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
 
@@ -355,12 +368,21 @@
     /**
      * Set the current configuration to the actual SystemProperties.
      */
-    public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) {
-        var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true);
+    private static void setSystemProperties(RavenwoodSystemProperties systemProperties) {
+        SystemProperties.clearChangeCallbacksForTest();
+        RavenwoodRuntimeNative.clearSystemProperties();
+        sProps = new RavenwoodSystemProperties(systemProperties, true);
+        for (var entry : systemProperties.getValues().entrySet()) {
+            RavenwoodRuntimeNative.setSystemProperty(entry.getKey(), entry.getValue());
+        }
+    }
 
-        android.os.SystemProperties.init$ravenwood(
-                clone.getValues(),
-                clone.getKeyReadablePredicate(),
-                clone.getKeyWritablePredicate());
+    @SuppressWarnings("unused")  // Called from native code (ravenwood_sysprop.cpp)
+    private static void checkSystemPropertyAccess(String key, boolean write) {
+        boolean result = write ? sProps.isKeyWritable(key) : sProps.isKeyReadable(key);
+        if (!result) {
+            throw new IllegalArgumentException((write ? "Write" : "Read")
+                    + " access to system property '" + key + "' denied via RavenwoodConfig");
+        }
     }
 }
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
index 428eb57..b4b8715 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -84,10 +84,10 @@
         try {
             mOutputWriter = new PrintWriter(mOutputFile);
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
-        // Crete the "latest" symlink.
+        // Create the "latest" symlink.
         Path symlink = Paths.get(tmpdir, basename + "latest.csv");
         try {
             if (Files.exists(symlink)) {
@@ -96,7 +96,7 @@
             Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
 
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
         Log.i(TAG, "Test result stats file: " + mOutputFile);
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index ef8f584..f1e1ef6 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -22,7 +22,6 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Predicate;
 
 public class RavenwoodSystemProperties {
     private volatile boolean mIsImmutable;
@@ -31,47 +30,9 @@
 
     /** Set of additional keys that should be considered readable */
     private final Set<String> mKeyReadable = new HashSet<>();
-    private final Predicate<String> mKeyReadablePredicate = (key) -> {
-        final String root = getKeyRoot(key);
-
-        if (root.startsWith("debug.")) return true;
-
-        // This set is carefully curated to help identify situations where a test may
-        // accidentally depend on a default value of an obscure property whose owner hasn't
-        // decided how Ravenwood should behave.
-        if (root.startsWith("boot.")) return true;
-        if (root.startsWith("build.")) return true;
-        if (root.startsWith("product.")) return true;
-        if (root.startsWith("soc.")) return true;
-        if (root.startsWith("system.")) return true;
-
-        switch (key) {
-            case "gsm.version.baseband":
-            case "no.such.thing":
-            case "qemu.sf.lcd_density":
-            case "ro.bootloader":
-            case "ro.debuggable":
-            case "ro.hardware":
-            case "ro.hw_timeout_multiplier":
-            case "ro.odm.build.media_performance_class":
-            case "ro.sf.lcd_density":
-            case "ro.treble.enabled":
-            case "ro.vndk.version":
-                return true;
-        }
-
-        return mKeyReadable.contains(key);
-    };
 
     /** Set of additional keys that should be considered writable */
     private final Set<String> mKeyWritable = new HashSet<>();
-    private final Predicate<String> mKeyWritablePredicate = (key) -> {
-        final String root = getKeyRoot(key);
-
-        if (root.startsWith("debug.")) return true;
-
-        return mKeyWritable.contains(key);
-    };
 
     public RavenwoodSystemProperties() {
         // TODO: load these values from build.prop generated files
@@ -121,12 +82,45 @@
         return new HashMap<>(mValues);
     }
 
-    public Predicate<String> getKeyReadablePredicate() {
-        return mKeyReadablePredicate;
+    public boolean isKeyReadable(String key) {
+        final String root = getKeyRoot(key);
+
+        if (root.startsWith("debug.")) return true;
+
+        // This set is carefully curated to help identify situations where a test may
+        // accidentally depend on a default value of an obscure property whose owner hasn't
+        // decided how Ravenwood should behave.
+        if (root.startsWith("boot.")) return true;
+        if (root.startsWith("build.")) return true;
+        if (root.startsWith("product.")) return true;
+        if (root.startsWith("soc.")) return true;
+        if (root.startsWith("system.")) return true;
+
+        switch (key) {
+            case "gsm.version.baseband":
+            case "no.such.thing":
+            case "qemu.sf.lcd_density":
+            case "ro.bootloader":
+            case "ro.debuggable":
+            case "ro.hardware":
+            case "ro.hw_timeout_multiplier":
+            case "ro.odm.build.media_performance_class":
+            case "ro.sf.lcd_density":
+            case "ro.treble.enabled":
+            case "ro.vndk.version":
+            case "ro.icu.data.path":
+                return true;
+        }
+
+        return mKeyReadable.contains(key);
     }
 
-    public Predicate<String> getKeyWritablePredicate() {
-        return mKeyWritablePredicate;
+    public boolean isKeyWritable(String key) {
+        final String root = getKeyRoot(key);
+
+        if (root.startsWith("debug.")) return true;
+
+        return mKeyWritable.contains(key);
     }
 
     private static final String[] PARTITIONS = {
@@ -208,4 +202,4 @@
     // Create a default instance, and make an immutable copy of it.
     public static final RavenwoodSystemProperties DEFAULT_VALUES =
             new RavenwoodSystemProperties(new RavenwoodSystemProperties(), true);
-}
\ No newline at end of file
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index 989bb6b..ef795c6 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -47,9 +47,6 @@
     public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
             "RAVENWOOD_VERBOSE"));
 
-    /** Name of `libravenwood_runtime` */
-    private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
-
     /** Directory name of `out/host/linux-x86/testcases/ravenwood-runtime` */
     private static final String RAVENWOOD_RUNTIME_DIR_NAME = "ravenwood-runtime";
 
@@ -110,29 +107,21 @@
     }
 
     /**
-     * Load the main runtime JNI library.
-     */
-    public static void loadRavenwoodNativeRuntime() {
-        ensureOnRavenwood();
-        loadJniLibrary(RAVENWOOD_NATIVE_RUNTIME_NAME);
-    }
-
-    /**
      * Internal implementation of
      * {@link android.platform.test.ravenwood.RavenwoodUtils#loadJniLibrary(String)}
      */
     public static void loadJniLibrary(String libname) {
         if (RavenwoodCommonUtils.isOnRavenwood()) {
-            loadJniLibraryInternal(libname);
+            System.load(getJniLibraryPath(libname));
         } else {
             System.loadLibrary(libname);
         }
     }
 
     /**
-     * Function equivalent to ART's System.loadLibrary. See RavenwoodUtils for why we need it.
+     * Find the shared library path from java.library.path.
      */
-    private static void loadJniLibraryInternal(String libname) {
+    public static String getJniLibraryPath(String libname) {
         var path = System.getProperty("java.library.path");
         var filename = "lib" + libname + ".so";
 
@@ -140,22 +129,21 @@
 
         try {
             if (path == null) {
-                throw new UnsatisfiedLinkError("Cannot load library " + libname + "."
+                throw new UnsatisfiedLinkError("Cannot find library " + libname + "."
                         + " Property java.library.path not set!");
             }
             for (var dir : path.split(":")) {
                 var file = new File(dir + "/" + filename);
                 if (file.exists()) {
-                    System.load(file.getAbsolutePath());
-                    return;
+                    return file.getAbsolutePath();
                 }
             }
-            throw new UnsatisfiedLinkError("Library " + libname + " not found in "
-                    + "java.library.path: " + path);
         } catch (Throwable e) {
             dumpFiles(System.out);
             throw e;
         }
+        throw new UnsatisfiedLinkError("Library " + libname + " not found in "
+                + "java.library.path: " + path);
     }
 
     private static void dumpFiles(PrintStream out) {
diff --git a/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
deleted file mode 100644
index b09bf31..0000000
--- a/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.os;
-
-import android.util.SparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
-
-import java.util.Map;
-import java.util.Objects;
-import java.util.function.Predicate;
-
-public class SystemProperties_host {
-    private static final Object sLock = new Object();
-
-    /** Active system property values */
-    @GuardedBy("sLock")
-    private static Map<String, String> sValues;
-    /** Predicate tested to determine if a given key can be read. */
-    @GuardedBy("sLock")
-    private static Predicate<String> sKeyReadablePredicate;
-    /** Predicate tested to determine if a given key can be written. */
-    @GuardedBy("sLock")
-    private static Predicate<String> sKeyWritablePredicate;
-
-    /**
-     * Reverse mapping that provides a way back to an original key from the
-     * {@link System#identityHashCode(Object)} of {@link String#intern}.
-     */
-    @GuardedBy("sLock")
-    private static SparseArray<String> sKeyHandles = new SparseArray<>();
-
-    /**
-     * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are
-     * set yet.
-     */
-    public static void initializeIfNeeded(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
-        synchronized (sLock) {
-            if (sValues != null) {
-                return; // Already initialized.
-            }
-            init$ravenwood(values, keyReadablePredicate, keyWritablePredicate);
-        }
-    }
-
-    public static void init$ravenwood(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
-        synchronized (sLock) {
-            sValues = Objects.requireNonNull(values);
-            sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
-            sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
-            sKeyHandles.clear();
-            synchronized (SystemProperties.sChangeCallbacks) {
-                SystemProperties.sChangeCallbacks.clear();
-            }
-        }
-    }
-
-    public static void reset$ravenwood() {
-        synchronized (sLock) {
-            sValues = null;
-            sKeyReadablePredicate = null;
-            sKeyWritablePredicate = null;
-            sKeyHandles.clear();
-            synchronized (SystemProperties.sChangeCallbacks) {
-                SystemProperties.sChangeCallbacks.clear();
-            }
-        }
-    }
-
-    public static void native_set(String key, String val) {
-        synchronized (sLock) {
-            Objects.requireNonNull(key);
-            Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            if (!sKeyWritablePredicate.test(key)) {
-                throw new IllegalArgumentException(
-                        "Write access to system property '" + key + "' denied via RavenwoodRule");
-            }
-            if (key.startsWith("ro.") && sValues.containsKey(key)) {
-                throw new IllegalArgumentException(
-                        "System property '" + key + "' already defined once; cannot redefine");
-            }
-            if ((val == null) || val.isEmpty()) {
-                sValues.remove(key);
-            } else {
-                sValues.put(key, val);
-            }
-            SystemProperties.callChangeCallbacks();
-        }
-    }
-
-    public static String native_get(String key, String def) {
-        synchronized (sLock) {
-            Objects.requireNonNull(key);
-            Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            if (!sKeyReadablePredicate.test(key)) {
-                throw new IllegalArgumentException(
-                        "Read access to system property '" + key + "' denied via RavenwoodRule");
-            }
-            return sValues.getOrDefault(key, def);
-        }
-    }
-
-    public static int native_get_int(String key, int def) {
-        try {
-            return Integer.parseInt(native_get(key, ""));
-        } catch (NumberFormatException ignored) {
-            return def;
-        }
-    }
-
-    public static long native_get_long(String key, long def) {
-        try {
-            return Long.parseLong(native_get(key, ""));
-        } catch (NumberFormatException ignored) {
-            return def;
-        }
-    }
-
-    public static boolean native_get_boolean(String key, boolean def) {
-        return parseBoolean(native_get(key, ""), def);
-    }
-
-    public static long native_find(String name) {
-        synchronized (sLock) {
-            Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            if (sValues.containsKey(name)) {
-                name = name.intern();
-                final int handle = System.identityHashCode(name);
-                sKeyHandles.put(handle, name);
-                return handle;
-            } else {
-                return 0;
-            }
-        }
-    }
-
-    public static String native_get(long handle) {
-        synchronized (sLock) {
-            return native_get(sKeyHandles.get((int) handle), "");
-        }
-    }
-
-    public static int native_get_int(long handle, int def) {
-        synchronized (sLock) {
-            return native_get_int(sKeyHandles.get((int) handle), def);
-        }
-    }
-
-    public static long native_get_long(long handle, long def) {
-        synchronized (sLock) {
-            return native_get_long(sKeyHandles.get((int) handle), def);
-        }
-    }
-
-    public static boolean native_get_boolean(long handle, boolean def) {
-        synchronized (sLock) {
-            return native_get_boolean(sKeyHandles.get((int) handle), def);
-        }
-    }
-
-    public static void native_add_change_callback() {
-        // Ignored; callback always registered via init above
-    }
-
-    public static void native_report_sysprop_change() {
-        // Report through callback always registered via init above
-        synchronized (sLock) {
-            Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            SystemProperties.callChangeCallbacks();
-        }
-    }
-
-    private static boolean parseBoolean(String val, boolean def) {
-        // Matches system/libbase/include/android-base/parsebool.h
-        if (val == null) return def;
-        switch (val) {
-            case "1":
-            case "on":
-            case "true":
-            case "y":
-            case "yes":
-                return true;
-            case "0":
-            case "false":
-            case "n":
-            case "no":
-            case "off":
-                return false;
-            default:
-                return def;
-        }
-    }
-}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java
index c56ec8a..3fedc1a 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java
@@ -15,8 +15,6 @@
  */
 package android.system;
 
-import com.android.ravenwood.common.RavenwoodCommonUtils;
-
 /**
  * Copied from libcore's version, with the local changes:
  * - All the imports are removed. (they're only used in javadoc)
@@ -1252,8 +1250,6 @@
     private static int placeholder() { return 0; }
     // ...because we want to initialize them at runtime.
     static {
-        // [ravenwood-change] Load the JNI lib.
-        RavenwoodCommonUtils.loadRavenwoodNativeRuntime();
         Native.initConstants();
     }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
index ad80d92..f13189f 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
@@ -19,7 +19,6 @@
 import android.system.StructStat;
 
 import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import java.io.FileDescriptor;
 
@@ -30,11 +29,6 @@
     private RavenwoodRuntimeNative() {
     }
 
-    static {
-        RavenwoodCommonUtils.ensureOnRavenwood();
-        RavenwoodCommonUtils.loadRavenwoodNativeRuntime();
-    }
-
     public static native void applyFreeFunction(long freeFunction, long nativePtr);
 
     private static native long nLseek(int fd, long offset, int whence) throws ErrnoException;
@@ -56,6 +50,14 @@
     public static native void setenv(String name, String value, boolean overwrite)
             throws ErrnoException;
 
+    public static native void reloadNativeLibrary(String libFile);
+
+    public static native String getSystemProperty(String key);
+
+    public static native boolean setSystemProperty(String key, String value);
+
+    public static native void clearSystemProperties();
+
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
diff --git a/ravenwood/runtime-jni/jni_helper.h b/ravenwood/runtime-jni/jni_helper.h
new file mode 100644
index 0000000..561fb3b
--- /dev/null
+++ b/ravenwood/runtime-jni/jni_helper.h
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+#include <jni.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+#include <nativehelper/ScopedUtfChars.h>
+
+#include <string>
+
+constexpr const char* kCommonUtils = "com/android/ravenwood/common/RavenwoodCommonUtils";
+constexpr const char* kRuntimeEnvController =
+        "android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController";
+constexpr const char* kRuntimeNative = "com/android/ravenwood/RavenwoodRuntimeNative";
+
+// We have to explicitly decode the string to real UTF-8, because when using GetStringUTFChars
+// we only get modified UTF-8, which is not the platform string type used in host JVM.
+struct ScopedRealUtf8Chars {
+    ScopedRealUtf8Chars(JNIEnv* env, jstring s) : valid_(false) {
+        if (s == nullptr) {
+            jniThrowNullPointerException(env);
+            return;
+        }
+        jclass clazz = env->GetObjectClass(s);
+        jmethodID getBytes = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
+
+        ScopedLocalRef<jstring> utf8(env, env->NewStringUTF("UTF-8"));
+        ScopedLocalRef<jbyteArray> jbytes(env,
+                                          (jbyteArray)env->CallObjectMethod(s, getBytes,
+                                                                            utf8.get()));
+
+        ScopedByteArrayRO bytes(env, jbytes.get());
+        string_.append((const char*)bytes.get(), bytes.size());
+        valid_ = true;
+    }
+
+    const char* c_str() const {
+        return valid_ ? string_.c_str() : nullptr;
+    }
+
+    size_t size() const {
+        return string_.size();
+    }
+
+    const char& operator[](size_t n) const {
+        return string_[n];
+    }
+
+private:
+    std::string string_;
+    bool valid_;
+};
+
+static inline JNIEnv* GetJNIEnvOrDie(JavaVM* vm) {
+    JNIEnv* env = nullptr;
+    vm->GetEnv((void**)&env, JNI_VERSION_1_4);
+    LOG_ALWAYS_FATAL_IF(env == nullptr, "Could not retrieve JNIEnv.");
+    return env;
+}
+
+static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) {
+    jclass clazz = env->FindClass(class_name);
+    LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name);
+    return clazz;
+}
+
+template <typename T>
+static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) {
+    jobject res = env->NewGlobalRef(in);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference.");
+    return static_cast<T>(res);
+}
+
+static inline jclass FindGlobalClassOrDie(JNIEnv* env, const char* class_name) {
+    return MakeGlobalRefOrDie(env, FindClassOrDie(env, class_name));
+}
+
+static inline jmethodID GetStaticMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name,
+                                               const char* method_signature) {
+    jmethodID res = env->GetStaticMethodID(clazz, method_name, method_signature);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static method %s with signature %s",
+                        method_name, method_signature);
+    return res;
+}
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index c255be5..3ff0848 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -15,21 +15,14 @@
  */
 
 #include <fcntl.h>
-#include <sys/stat.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <unistd.h>
+#include <utils/misc.h>
+
 #include <string>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedLocalRef.h>
-#include <nativehelper/ScopedUtfChars.h>
-#include <nativehelper/ScopedPrimitiveArray.h>
 
-#include "jni.h"
-#include "utils/Log.h"
-#include "utils/misc.h"
-
-// Defined in ravenwood_os_constants.cpp
-void register_android_system_OsConstants(JNIEnv* env);
+#include "jni_helper.h"
 
 // ---- Exception related ----
 
@@ -51,53 +44,6 @@
 static jclass g_StructStat;
 static jclass g_StructTimespecClass;
 
-// We have to explicitly decode the string to real UTF-8, because when using GetStringUTFChars
-// we only get modified UTF-8, which is not the platform string type used in host JVM.
-struct ScopedRealUtf8Chars {
-    ScopedRealUtf8Chars(JNIEnv* env, jstring s) : valid_(false) {
-        if (s == nullptr) {
-            jniThrowNullPointerException(env);
-            return;
-        }
-        jclass clazz = env->GetObjectClass(s);
-        jmethodID getBytes = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
-
-        ScopedLocalRef<jstring> utf8(env, env->NewStringUTF("UTF-8"));
-        ScopedLocalRef<jbyteArray> jbytes(env,
-            (jbyteArray) env->CallObjectMethod(s, getBytes, utf8.get()));
-
-        ScopedByteArrayRO bytes(env, jbytes.get());
-        string_.append((const char *) bytes.get(), bytes.size());
-        valid_ = true;
-    }
-
-    const char* c_str() const {
-        return valid_ ? string_.c_str() : nullptr;
-    }
-
-    size_t size() const {
-        return string_.size();
-    }
-
-    const char& operator[](size_t n) const {
-        return string_[n];
-    }
-
-private:
-    std::string string_;
-    bool valid_;
-};
-
-static jclass findClass(JNIEnv* env, const char* name) {
-    ScopedLocalRef<jclass> localClass(env, env->FindClass(name));
-    jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
-    if (result == NULL) {
-        ALOGE("failed to find class '%s'", name);
-        abort();
-    }
-    return result;
-}
-
 static jobject makeStructTimespec(JNIEnv* env, const struct timespec& ts) {
     static jmethodID ctor = env->GetMethodID(g_StructTimespecClass, "<init>",
             "(JJ)V");
@@ -229,6 +175,8 @@
 
 // ---- Registration ----
 
+extern void register_android_system_OsConstants(JNIEnv* env);
+
 static const JNINativeMethod sMethods[] =
 {
     { "applyFreeFunction", "(JJ)V", (void*)nApplyFreeFunction },
@@ -243,24 +191,14 @@
     { "setenv", "(Ljava/lang/String;Ljava/lang/String;Z)V", (void*)Linux_setenv },
 };
 
-extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
-{
-    JNIEnv* env = NULL;
-    jint result = -1;
-
-    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
-        ALOGE("GetEnv failed!");
-        return result;
-    }
-    ALOG_ASSERT(env, "Could not retrieve the env!");
-
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
     ALOGI("%s: JNI_OnLoad", __FILE__);
 
-    g_StructStat = findClass(env, "android/system/StructStat");
-    g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
+    JNIEnv* env = GetJNIEnvOrDie(vm);
+    g_StructStat = FindGlobalClassOrDie(env, "android/system/StructStat");
+    g_StructTimespecClass = FindGlobalClassOrDie(env, "android/system/StructTimespec");
 
-    jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/RavenwoodRuntimeNative",
-            sMethods, NELEM(sMethods));
+    jint res = jniRegisterNativeMethods(env, kRuntimeNative, sMethods, NELEM(sMethods));
     if (res < 0) {
         return res;
     }
diff --git a/ravenwood/runtime-jni/ravenwood_sysprop.cpp b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
new file mode 100644
index 0000000..aafc426
--- /dev/null
+++ b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
@@ -0,0 +1,187 @@
+/*
+ * 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.
+ */
+
+#include <dlfcn.h>
+
+#include <set>
+
+#include "jni_helper.h"
+
+// Implement a rudimentary system properties data store
+
+#define PROP_VALUE_MAX 92
+
+namespace {
+
+struct prop_info {
+    std::string key;
+    mutable std::string value;
+    mutable uint32_t serial;
+
+    prop_info(const char* key, const char* value) : key(key), value(value), serial(0) {}
+};
+
+struct prop_info_cmp {
+    using is_transparent = void;
+    bool operator()(const prop_info& lhs, const prop_info& rhs) {
+        return lhs.key < rhs.key;
+    }
+    bool operator()(std::string_view lhs, const prop_info& rhs) {
+        return lhs < rhs.key;
+    }
+    bool operator()(const prop_info& lhs, std::string_view rhs) {
+        return lhs.key < rhs;
+    }
+};
+
+} // namespace
+
+static auto& g_properties_lock = *new std::mutex;
+static auto& g_properties = *new std::set<prop_info, prop_info_cmp>;
+
+static bool property_set(const char* key, const char* value) {
+    if (key == nullptr || *key == '\0') return false;
+    if (value == nullptr) value = "";
+    bool read_only = !strncmp(key, "ro.", 3);
+    if (!read_only && strlen(value) >= PROP_VALUE_MAX) return false;
+
+    std::lock_guard lock(g_properties_lock);
+    auto [it, success] = g_properties.emplace(key, value);
+    if (read_only) return success;
+    if (!success) {
+        it->value = value;
+        ++it->serial;
+    }
+    return true;
+}
+
+template <typename Func>
+static void property_get(const char* key, Func callback) {
+    std::lock_guard lock(g_properties_lock);
+    auto it = g_properties.find(key);
+    if (it != g_properties.end()) {
+        callback(*it);
+    }
+}
+
+// Redefine the __system_property_XXX functions here so we can perform
+// logging and access checks for all sysprops in native code.
+
+static void check_system_property_access(const char* key, bool write);
+
+extern "C" {
+
+int __system_property_set(const char* key, const char* value) {
+    check_system_property_access(key, true);
+    return property_set(key, value) ? 0 : -1;
+}
+
+int __system_property_get(const char* key, char* value) {
+    check_system_property_access(key, false);
+    *value = '\0';
+    property_get(key, [&](const prop_info& info) {
+        snprintf(value, PROP_VALUE_MAX, "%s", info.value.c_str());
+    });
+    return strlen(value);
+}
+
+const prop_info* __system_property_find(const char* key) {
+    check_system_property_access(key, false);
+    const prop_info* pi = nullptr;
+    property_get(key, [&](const prop_info& info) { pi = &info; });
+    return pi;
+}
+
+void __system_property_read_callback(const prop_info* pi,
+                                     void (*callback)(void*, const char*, const char*, uint32_t),
+                                     void* cookie) {
+    std::lock_guard lock(g_properties_lock);
+    callback(cookie, pi->key.c_str(), pi->value.c_str(), pi->serial);
+}
+
+} // extern "C"
+
+// ---- JNI ----
+
+static JavaVM* gVM = nullptr;
+static jclass gEnvController = nullptr;
+static jmethodID gCheckSystemPropertyAccess;
+
+static void reloadNativeLibrary(JNIEnv* env, jclass, jstring javaPath) {
+    ScopedUtfChars path(env, javaPath);
+    // Force reload ourselves as global
+    dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL | RTLD_NOLOAD);
+}
+
+// Call back into Java code to check property access
+static void check_system_property_access(const char* key, bool write) {
+    if (gVM != nullptr && gEnvController != nullptr) {
+        JNIEnv* env;
+        if (gVM->GetEnv((void**)&env, JNI_VERSION_1_4) >= 0) {
+            ALOGI("%s access to system property '%s'", write ? "Write" : "Read", key);
+            env->CallStaticVoidMethod(gEnvController, gCheckSystemPropertyAccess,
+                                      env->NewStringUTF(key), write ? JNI_TRUE : JNI_FALSE);
+            return;
+        }
+    }
+    // Not on JVM thread, abort
+    LOG_ALWAYS_FATAL("Access to system property '%s' on non-JVM threads is not allowed.", key);
+}
+
+static jstring getSystemProperty(JNIEnv* env, jclass, jstring javaKey) {
+    ScopedUtfChars key(env, javaKey);
+    jstring value = nullptr;
+    property_get(key.c_str(),
+                 [&](const prop_info& info) { value = env->NewStringUTF(info.value.c_str()); });
+    return value;
+}
+
+static jboolean setSystemProperty(JNIEnv* env, jclass, jstring javaKey, jstring javaValue) {
+    ScopedUtfChars key(env, javaKey);
+    ScopedUtfChars value(env, javaValue);
+    return property_set(key.c_str(), value.c_str()) ? JNI_TRUE : JNI_FALSE;
+}
+
+static void clearSystemProperties(JNIEnv*, jclass) {
+    std::lock_guard lock(g_properties_lock);
+    g_properties.clear();
+}
+
+static const JNINativeMethod sMethods[] = {
+        {"reloadNativeLibrary", "(Ljava/lang/String;)V", (void*)reloadNativeLibrary},
+        {"getSystemProperty", "(Ljava/lang/String;)Ljava/lang/String;", (void*)getSystemProperty},
+        {"setSystemProperty", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)setSystemProperty},
+        {"clearSystemProperties", "()V", (void*)clearSystemProperties},
+};
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+    ALOGI("%s: JNI_OnLoad", __FILE__);
+
+    JNIEnv* env = GetJNIEnvOrDie(vm);
+    gVM = vm;
+
+    // Fetch several references for future use
+    gEnvController = FindGlobalClassOrDie(env, kRuntimeEnvController);
+    gCheckSystemPropertyAccess =
+            GetStaticMethodIDOrDie(env, gEnvController, "checkSystemPropertyAccess",
+                                   "(Ljava/lang/String;Z)V");
+
+    // Expose raw property methods as JNI methods
+    jint res = jniRegisterNativeMethods(env, kRuntimeNative, sMethods, NELEM(sMethods));
+    if (res < 0) return -1;
+
+    return JNI_VERSION_1_4;
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 3224b27..73b7b35 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -165,16 +165,27 @@
     protected final AccessibilitySecurityPolicy mSecurityPolicy;
     protected final AccessibilityTrace mTrace;
 
-    // The attribution tag set by the service that is bound to this instance
+    /** The attribution tag set by the client that is bound to this instance */
     protected String mAttributionTag;
 
     protected int mDisplayTypes = DISPLAY_TYPE_DEFAULT;
 
-    // The service that's bound to this instance. Whenever this value is non-null, this
-    // object is registered as a death recipient
-    IBinder mService;
+    /**
+     * Binder of the {@link #mClient}.
+     *
+     * <p>Whenever this value is non-null, it should be registered as a {@link
+     * IBinder.DeathRecipient}
+     */
+    @Nullable IBinder mClientBinder;
 
-    IAccessibilityServiceClient mServiceInterface;
+    /**
+     * The accessibility client this class represents.
+     *
+     * <p>The client is in the application process, i.e., it's a client of system_server. Depending
+     * on the use case, the client can be an {@link AccessibilityService}, a {@code UiAutomation},
+     * etc.
+     */
+    @Nullable IAccessibilityServiceClient mClient;
 
     int mEventTypes;
 
@@ -218,10 +229,10 @@
     int mGenericMotionEventSources;
     int mObservedMotionEventSources;
 
-    // the events pending events to be dispatched to this service
+    /** Pending events to be dispatched to the client */
     final SparseArray<AccessibilityEvent> mPendingEvents = new SparseArray<>();
 
-    /** Whether this service relies on its {@link AccessibilityCache} being up to date */
+    /** Whether the client relies on its {@link AccessibilityCache} being up to date */
     boolean mUsesAccessibilityCache = false;
 
     // Handler only for dispatching accessibility events since we use event
@@ -230,7 +241,7 @@
 
     final SparseArray<IBinder> mOverlayWindowTokens = new SparseArray();
 
-    // All the embedded accessibility overlays that have been added by this service.
+    /** All the embedded accessibility overlays that have been added by the client. */
     private List<SurfaceControl> mOverlays = new ArrayList<>();
 
     /** The timestamp of requesting to take screenshot in milliseconds */
@@ -274,7 +285,8 @@
 
         /**
          * Called back to notify system that the client has changed
-         * @param serviceInfoChanged True if the service's AccessibilityServiceInfo changed.
+         *
+         * @param serviceInfoChanged True if the client's AccessibilityServiceInfo changed.
          */
         void onClientChangeLocked(boolean serviceInfoChanged);
 
@@ -360,21 +372,22 @@
         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mIPlatformCompat = IPlatformCompat.Stub.asInterface(
                 ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
-        mEventDispatchHandler = new Handler(mainHandler.getLooper()) {
-            @Override
-            public void handleMessage(Message message) {
-                final int eventType =  message.what;
-                AccessibilityEvent event = (AccessibilityEvent) message.obj;
-                boolean serviceWantsEvent = message.arg1 != 0;
-                notifyAccessibilityEventInternal(eventType, event, serviceWantsEvent);
-            }
-        };
+        mEventDispatchHandler =
+                new Handler(mainHandler.getLooper()) {
+                    @Override
+                    public void handleMessage(Message message) {
+                        final int eventType = message.what;
+                        AccessibilityEvent event = (AccessibilityEvent) message.obj;
+                        boolean clientWantsEvent = message.arg1 != 0;
+                        notifyAccessibilityEventInternal(eventType, event, clientWantsEvent);
+                    }
+                };
         setDynamicallyConfigurableProperties(accessibilityServiceInfo);
     }
 
     @Override
     public boolean onKeyEvent(KeyEvent keyEvent, int sequenceNumber) {
-        if (!mRequestFilterKeyEvents || (mServiceInterface == null)) {
+        if (!mRequestFilterKeyEvents || (mClient == null)) {
             return false;
         }
         if((mAccessibilityServiceInfo.getCapabilities()
@@ -388,7 +401,7 @@
             if (svcClientTracingEnabled()) {
                 logTraceSvcClient("onKeyEvent", keyEvent + ", " + sequenceNumber);
             }
-            mServiceInterface.onKeyEvent(keyEvent, sequenceNumber);
+            mClient.onKeyEvent(keyEvent, sequenceNumber);
         } catch (RemoteException e) {
             return false;
         }
@@ -470,7 +483,7 @@
     }
 
     public boolean canReceiveEventsLocked() {
-        return (mEventTypes != 0 && mService != null);
+        return (mEventTypes != 0 && mClientBinder != null);
     }
 
     @RequiresNoPermission
@@ -520,7 +533,7 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                // If the XML manifest had data to configure the service its info
+                // If the XML manifest had data to configure the AccessibilityService, its info
                 // should be already set. In such a case update only the dynamically
                 // configurable properties.
                 boolean oldRequestIme = mRequestImeApis;
@@ -1733,40 +1746,40 @@
         try {
             // Clear the proxy in the other process so this
             // IAccessibilityServiceConnection can be garbage collected.
-            if (mServiceInterface != null) {
+            if (mClient != null) {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("init", "null, " + mId + ", null");
                 }
-                mServiceInterface.init(null, mId, null);
+                mClient.init(null, mId, null);
             }
         } catch (RemoteException re) {
                 /* ignore */
         }
-        if (mService != null) {
+        if (mClientBinder != null) {
             try {
-                mService.unlinkToDeath(this, 0);
+                mClientBinder.unlinkToDeath(this, 0);
             } catch (NoSuchElementException e) {
                 Slog.e(LOG_TAG, "Failed unregistering death link");
             }
-            mService = null;
+            mClientBinder = null;
         }
 
-        mServiceInterface = null;
+        mClient = null;
         mReceivedAccessibilityButtonCallbackSinceBind = false;
     }
 
     public boolean isConnectedLocked() {
-        return (mService != null);
+        return (mClientBinder != null);
     }
 
     public void notifyAccessibilityEvent(AccessibilityEvent event) {
         synchronized (mLock) {
             final int eventType = event.getEventType();
 
-            final boolean serviceWantsEvent = wantsEventLocked(event);
+            final boolean clientWantsEvent = clientWantsEventLocked(event);
             final boolean requiredForCacheConsistency = mUsesAccessibilityCache
                     && ((AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK & eventType) != 0);
-            if (!serviceWantsEvent && !requiredForCacheConsistency) {
+            if (!clientWantsEvent && !requiredForCacheConsistency) {
                 return;
             }
 
@@ -1774,7 +1787,7 @@
                 return;
             }
             // Make a copy since during dispatch it is possible the event to
-            // be modified to remove its source if the receiving service does
+            // be modified to remove its source if the receiving client does
             // not have permission to access the window content.
             AccessibilityEvent newEvent = AccessibilityEvent.obtain(event);
             Message message;
@@ -1792,22 +1805,20 @@
                 // Send all messages, bypassing mPendingEvents
                 message = mEventDispatchHandler.obtainMessage(eventType, newEvent);
             }
-            message.arg1 = serviceWantsEvent ? 1 : 0;
+            message.arg1 = clientWantsEvent ? 1 : 0;
 
             mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout);
         }
     }
 
     /**
-     * Determines if given event can be dispatched to a service based on the package of the
-     * event source. Specifically, a service is notified if it is interested in events from the
-     * package.
+     * Determines if given event can be dispatched to a client based on the package of the event
+     * source. Specifically, a client is notified if it is interested in events from the package.
      *
      * @param event The event.
-     * @return True if the listener should be notified, false otherwise.
+     * @return True if the client should be notified, false otherwise.
      */
-    private boolean wantsEventLocked(AccessibilityEvent event) {
-
+    private boolean clientWantsEventLocked(AccessibilityEvent event) {
         if (!canReceiveEventsLocked()) {
             return false;
         }
@@ -1838,22 +1849,20 @@
     }
 
     /**
-     * Notifies an accessibility service client for a scheduled event given the event type.
+     * Notifies a client for a scheduled event given the event type.
      *
      * @param eventType The type of the event to dispatch.
      */
     private void notifyAccessibilityEventInternal(
-            int eventType,
-            AccessibilityEvent event,
-            boolean serviceWantsEvent) {
-        IAccessibilityServiceClient listener;
+            int eventType, AccessibilityEvent event, boolean clientWantsEvent) {
+        IAccessibilityServiceClient client;
 
         synchronized (mLock) {
-            listener = mServiceInterface;
+            client = mClient;
 
-            // If the service died/was disabled while the message for dispatching
-            // the accessibility event was propagating the listener may be null.
-            if (listener == null) {
+            // If the client (in the application process) died/was disabled while the message for
+            // dispatching the accessibility event was propagating, "client" may be null.
+            if (client == null) {
                 return;
             }
 
@@ -1868,7 +1877,7 @@
                 //   1) A binder thread calls notifyAccessibilityServiceDelayedLocked
                 //      which posts a message for dispatching an event and stores the event
                 //      in mPendingEvents.
-                //   2) The message is pulled from the queue by the handler on the service
+                //   2) The message is pulled from the queue by the handler on the client
                 //      thread and this method is just about to acquire the lock.
                 //   3) Another binder thread acquires the lock in notifyAccessibilityEvent
                 //   4) notifyAccessibilityEvent recycles the event that this method was about
@@ -1876,7 +1885,7 @@
                 //   5) This method grabs the new event, processes it, and removes it from
                 //      mPendingEvents
                 //   6) The second message dispatched in (4) arrives, but the event has been
-                //      remvoved in (5).
+                //      removed in (5).
                 event = mPendingEvents.get(eventType);
                 if (event == null) {
                     return;
@@ -1893,14 +1902,14 @@
 
         try {
             if (svcClientTracingEnabled()) {
-                logTraceSvcClient("onAccessibilityEvent", event + ";" + serviceWantsEvent);
+                logTraceSvcClient("onAccessibilityEvent", event + ";" + clientWantsEvent);
             }
-            listener.onAccessibilityEvent(event, serviceWantsEvent);
+            client.onAccessibilityEvent(event, clientWantsEvent);
             if (DEBUG) {
-                Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
+                Slog.i(LOG_TAG, "Event " + event + " sent to " + client);
             }
         } catch (RemoteException re) {
-            Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re);
+            Slog.e(LOG_TAG, "Error during sending " + event + " to " + client, re);
         } finally {
             event.recycle();
         }
@@ -1978,122 +1987,126 @@
         return (mGenericMotionEventSources & eventSourceWithoutClass) != 0;
     }
 
-
     /**
-     * Called by the invocation handler to notify the service that the
-     * state of magnification has changed.
+     * Called by the invocation handler to notify the client that the state of magnification has
+     * changed.
      */
-    private void notifyMagnificationChangedInternal(int displayId, @NonNull Region region,
-            @NonNull MagnificationConfig config) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+    private void notifyMagnificationChangedInternal(
+            int displayId, @NonNull Region region, @NonNull MagnificationConfig config) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onMagnificationChanged", displayId + ", " + region + ", "
                             + config.toString());
                 }
-                listener.onMagnificationChanged(displayId, region, config);
+                client.onMagnificationChanged(displayId, region, config);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending magnification changes to " + mService, re);
+                Slog.e(LOG_TAG, "Error sending magnification changes to " + mClientBinder, re);
             }
         }
     }
 
     /**
-     * Called by the invocation handler to notify the service that the state of the soft
-     * keyboard show mode has changed.
+     * Called by the invocation handler to notify the client that the state of the soft keyboard
+     * show mode has changed.
      */
     private void notifySoftKeyboardShowModeChangedInternal(int showState) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onSoftKeyboardShowModeChanged", String.valueOf(showState));
                 }
-                listener.onSoftKeyboardShowModeChanged(showState);
+                client.onSoftKeyboardShowModeChanged(showState);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending soft keyboard show mode changes to " + mService,
+                Slog.e(
+                        LOG_TAG,
+                        "Error sending soft keyboard show mode changes to " + mClientBinder,
                         re);
             }
         }
     }
 
     private void notifyAccessibilityButtonClickedInternal(int displayId) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onAccessibilityButtonClicked", String.valueOf(displayId));
                 }
-                listener.onAccessibilityButtonClicked(displayId);
+                client.onAccessibilityButtonClicked(displayId);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending accessibility button click to " + mService, re);
+                Slog.e(LOG_TAG, "Error sending accessibility button click to " + mClientBinder, re);
             }
         }
     }
 
     private void notifyAccessibilityButtonAvailabilityChangedInternal(boolean available) {
-        // Only notify the service if it's not been notified or the state has changed
+        // Only notify the client if it's not been notified or the state has changed
         if (mReceivedAccessibilityButtonCallbackSinceBind
                 && (mLastAccessibilityButtonCallbackState == available)) {
             return;
         }
         mReceivedAccessibilityButtonCallbackSinceBind = true;
         mLastAccessibilityButtonCallbackState = available;
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onAccessibilityButtonAvailabilityChanged",
                             String.valueOf(available));
                 }
-                listener.onAccessibilityButtonAvailabilityChanged(available);
+                client.onAccessibilityButtonAvailabilityChanged(available);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error sending accessibility button availability change to " + mService,
+                Slog.e(
+                        LOG_TAG,
+                        "Error sending accessibility button availability change to "
+                                + mClientBinder,
                         re);
             }
         }
     }
 
     private void notifyGestureInternal(AccessibilityGestureEvent gestureInfo) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onGesture", gestureInfo.toString());
                 }
-                listener.onGesture(gestureInfo);
+                client.onGesture(gestureInfo);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error during sending gesture " + gestureInfo
-                        + " to " + mService, re);
-            }
-        }
-    }
-
-    private void notifySystemActionsChangedInternal() {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
-            try {
-                if (svcClientTracingEnabled()) {
-                    logTraceSvcClient("onSystemActionsChanged", "");
-                }
-                listener.onSystemActionsChanged();
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending system actions change to " + mService,
+                Slog.e(
+                        LOG_TAG,
+                        "Error during sending gesture " + gestureInfo + " to " + mClientBinder,
                         re);
             }
         }
     }
 
+    private void notifySystemActionsChangedInternal() {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
+            try {
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onSystemActionsChanged", "");
+                }
+                client.onSystemActionsChanged();
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error sending system actions change to " + mClientBinder, re);
+            }
+        }
+    }
+
     private void notifyClearAccessibilityCacheInternal() {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("clearAccessibilityCache", "");
                 }
-                listener.clearAccessibilityCache();
+                client.clearAccessibilityCache();
             } catch (RemoteException re) {
                 Slog.e(LOG_TAG, "Error during requesting accessibility info cache"
                         + " to be cleared.", re);
@@ -2106,70 +2119,66 @@
 
     private void setImeSessionEnabledInternal(IAccessibilityInputMethodSession session,
             boolean enabled) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null && session != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null && session != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("createImeSession", "");
                 }
-                listener.setImeSessionEnabled(session, enabled);
+                client.setImeSessionEnabled(session, enabled);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error requesting IME session from " + mService, re);
+                Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re);
             }
         }
     }
 
     private void bindInputInternal() {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("bindInput", "");
                 }
-                listener.bindInput();
+                client.bindInput();
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error binding input to " + mService, re);
+                Slog.e(LOG_TAG, "Error binding input to " + mClientBinder, re);
             }
         }
     }
 
     private void unbindInputInternal() {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("unbindInput", "");
                 }
-                listener.unbindInput();
+                client.unbindInput();
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error unbinding input to " + mService, re);
+                Slog.e(LOG_TAG, "Error unbinding input to " + mClientBinder, re);
             }
         }
     }
 
     private void startInputInternal(IRemoteAccessibilityInputConnection connection,
             EditorInfo editorInfo, boolean restarting) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("startInput", "editorInfo=" + editorInfo
                             + " restarting=" + restarting);
                 }
-                listener.startInput(connection, editorInfo, restarting);
+                client.startInput(connection, editorInfo, restarting);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error starting input to " + mService, re);
+                Slog.e(LOG_TAG, "Error starting input to " + mClientBinder, re);
             }
         }
     }
 
-    protected IAccessibilityServiceClient getServiceInterfaceSafely() {
+    protected IAccessibilityServiceClient getClientSafely() {
         synchronized (mLock) {
-            return mServiceInterface;
+            return mClient;
         }
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 7580b69..d595d02 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1435,8 +1435,8 @@
                 interfacesToInterrupt = new ArrayList<>(services.size());
                 for (int i = 0; i < services.size(); i++) {
                     AccessibilityServiceConnection service = services.get(i);
-                    IBinder a11yServiceBinder = service.mService;
-                    IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface;
+                    IBinder a11yServiceBinder = service.mClientBinder;
+                    IAccessibilityServiceClient a11yServiceInterface = service.mClient;
                     if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) {
                         interfacesToInterrupt.add(a11yServiceInterface);
                     }
@@ -4962,9 +4962,14 @@
                 if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
                         && android.security.Flags.extendEcmToAllSettings()) {
                     try {
-                        return !mContext.getSystemService(EnhancedConfirmationManager.class)
-                                .isRestricted(packageName,
-                                        AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+                        final EnhancedConfirmationManager userContextEcm =
+                                mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0)
+                                        .getSystemService(EnhancedConfirmationManager.class);
+                        if (userContextEcm != null) {
+                            return !userContextEcm.isRestricted(packageName,
+                                    AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+                        }
+                        return false;
                     } catch (PackageManager.NameNotFoundException e) {
                         Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e);
                         return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 786d167..15999d1 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -166,8 +166,9 @@
             if (userState.getBindInstantServiceAllowedLocked()) {
                 flags |= Context.BIND_ALLOW_INSTANT;
             }
-            if (mService == null && mContext.bindServiceAsUser(
-                    mIntent, this, flags, new UserHandle(userState.mUserId))) {
+            if (mClientBinder == null
+                    && mContext.bindServiceAsUser(
+                            mIntent, this, flags, new UserHandle(userState.mUserId))) {
                 userState.getBindingServicesLocked().add(mComponentName);
             }
         } finally {
@@ -227,20 +228,20 @@
             addWindowTokensForAllDisplays();
         }
         synchronized (mLock) {
-            if (mService != service) {
-                if (mService != null) {
-                    mService.unlinkToDeath(this, 0);
+            if (mClientBinder != service) {
+                if (mClientBinder != null) {
+                    mClientBinder.unlinkToDeath(this, 0);
                 }
-                mService = service;
+                mClientBinder = service;
                 try {
-                    mService.linkToDeath(this, 0);
+                    mClientBinder.linkToDeath(this, 0);
                 } catch (RemoteException re) {
                     Slog.e(LOG_TAG, "Failed registering death link");
                     binderDied();
                     return;
                 }
             }
-            mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service);
+            mClient = IAccessibilityServiceClient.Stub.asInterface(service);
             if (userState == null) return;
             userState.addServiceLocked(this);
             mSystemSupport.onClientChangeLocked(false);
@@ -261,7 +262,7 @@
     }
 
     private void initializeService() {
-        IAccessibilityServiceClient serviceInterface = null;
+        IAccessibilityServiceClient client = null;
         synchronized (mLock) {
             AccessibilityUserState userState = mUserStateWeakReference.get();
             if (userState == null) return;
@@ -272,18 +273,17 @@
                 bindingServices.remove(mComponentName);
                 crashedServices.remove(mComponentName);
                 mAccessibilityServiceInfo.crashed = false;
-                serviceInterface = mServiceInterface;
+                client = mClient;
             }
             // There's a chance that service is removed from enabled_accessibility_services setting
             // key, but skip unbinding because of it's in binding state. Unbinds it if it's
             // not in enabled service list.
-            if (serviceInterface != null
-                    && !userState.getEnabledServicesLocked().contains(mComponentName)) {
+            if (client != null && !userState.getEnabledServicesLocked().contains(mComponentName)) {
                 mSystemSupport.onClientChangeLocked(false);
                 return;
             }
         }
-        if (serviceInterface == null) {
+        if (client == null) {
             binderDied();
             return;
         }
@@ -292,10 +292,9 @@
                 logTraceSvcClient("init",
                         this + "," + mId + "," + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
             }
-            serviceInterface.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+            client.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
         } catch (RemoteException re) {
-            Slog.w(LOG_TAG, "Error while setting connection for service: "
-                    + serviceInterface, re);
+            Slog.w(LOG_TAG, "Error while setting connection for service: " + client, re);
             binderDied();
         }
     }
@@ -496,7 +495,7 @@
 
     @Override
     public boolean isCapturingFingerprintGestures() {
-        return (mServiceInterface != null)
+        return (mClient != null)
                 && mSecurityPolicy.canCaptureFingerprintGestures(this)
                 && mCaptureFingerprintGestures;
     }
@@ -506,17 +505,17 @@
         if (!isCapturingFingerprintGestures()) {
             return;
         }
-        IAccessibilityServiceClient serviceInterface;
+        IAccessibilityServiceClient client;
         synchronized (mLock) {
-            serviceInterface = mServiceInterface;
+            client = mClient;
         }
-        if (serviceInterface != null) {
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient(
                             "onFingerprintCapturingGesturesChanged", String.valueOf(active));
                 }
-                mServiceInterface.onFingerprintCapturingGesturesChanged(active);
+                mClient.onFingerprintCapturingGesturesChanged(active);
             } catch (RemoteException e) {
             }
         }
@@ -527,16 +526,16 @@
         if (!isCapturingFingerprintGestures()) {
             return;
         }
-        IAccessibilityServiceClient serviceInterface;
+        IAccessibilityServiceClient client;
         synchronized (mLock) {
-            serviceInterface = mServiceInterface;
+            client = mClient;
         }
-        if (serviceInterface != null) {
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("onFingerprintGesture", String.valueOf(gesture));
                 }
-                mServiceInterface.onFingerprintGesture(gesture);
+                mClient.onFingerprintGesture(gesture);
             } catch (RemoteException e) {
             }
         }
@@ -546,7 +545,7 @@
     @Override
     public void dispatchGesture(int sequence, ParceledListSlice gestureSteps, int displayId) {
         synchronized (mLock) {
-            if (mServiceInterface != null && mSecurityPolicy.canPerformGestures(this)) {
+            if (mClient != null && mSecurityPolicy.canPerformGestures(this)) {
                 final long identity = Binder.clearCallingIdentity();
                 try {
                     MotionEventInjector motionEventInjector =
@@ -557,16 +556,18 @@
                     if (motionEventInjector != null
                             && mWindowManagerService.isTouchOrFaketouchDevice()) {
                         motionEventInjector.injectEvents(
-                                gestureSteps.getList(), mServiceInterface, sequence, displayId);
+                                gestureSteps.getList(), mClient, sequence, displayId);
                     } else {
                         try {
                             if (svcClientTracingEnabled()) {
                                 logTraceSvcClient("onPerformGestureResult", sequence + ", false");
                             }
-                            mServiceInterface.onPerformGestureResult(sequence, false);
+                            mClient.onPerformGestureResult(sequence, false);
                         } catch (RemoteException re) {
-                            Slog.e(LOG_TAG, "Error sending motion event injection failure to "
-                                    + mServiceInterface, re);
+                            Slog.e(
+                                    LOG_TAG,
+                                    "Error sending motion event injection failure to " + mClient,
+                                    re);
                         }
                     }
                 } finally {
@@ -631,48 +632,47 @@
 
     @Override
     protected void createImeSessionInternal() {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (svcClientTracingEnabled()) {
                     logTraceSvcClient("createImeSession", "");
                 }
                 AccessibilityInputMethodSessionCallback
                         callback = new AccessibilityInputMethodSessionCallback(mUserId);
-                listener.createImeSession(callback);
+                client.createImeSession(callback);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG,
-                        "Error requesting IME session from " + mService, re);
+                Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re);
             }
         }
     }
 
     private void notifyMotionEventInternal(MotionEvent event) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (mTrace.isA11yTracingEnabled()) {
                     logTraceSvcClient(".onMotionEvent ",
                             event.toString());
                 }
-                listener.onMotionEvent(event);
+                client.onMotionEvent(event);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+                Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re);
             }
         }
     }
 
     private void notifyTouchStateInternal(int displayId, int state) {
-        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
-        if (listener != null) {
+        final IAccessibilityServiceClient client = getClientSafely();
+        if (client != null) {
             try {
                 if (mTrace.isA11yTracingEnabled()) {
                     logTraceSvcClient(".onTouchStateChanged ",
                             TouchInteractionController.stateToString(state));
                 }
-                listener.onTouchStateChanged(displayId, state);
+                client.onTouchStateChanged(displayId, state);
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+                Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re);
             }
         }
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 4cb3d24..cd97d83 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -109,14 +109,11 @@
         return mDeviceId;
     }
 
-    /**
-     * Called when the proxy is registered.
-     */
-    void initializeServiceInterface(IAccessibilityServiceClient serviceInterface)
-            throws RemoteException {
-        mServiceInterface = serviceInterface;
-        mService = serviceInterface.asBinder();
-        mServiceInterface.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId));
+    /** Called when the proxy is registered. */
+    void initializeClient(IAccessibilityServiceClient client) throws RemoteException {
+        mClient = client;
+        mClientBinder = client.asBinder();
+        mClient.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId));
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index b4deeb0..da11a76 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -214,7 +214,7 @@
                 mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId);
             }
         });
-        connection.initializeServiceInterface(client);
+        connection.initializeClient(client);
     }
 
     private void registerVirtualDeviceListener() {
@@ -561,8 +561,8 @@
             final ProxyAccessibilityServiceConnection proxy =
                     mProxyA11yServiceConnections.valueAt(i);
             if (proxy != null && proxy.getDeviceId() == deviceId) {
-                final IBinder proxyBinder = proxy.mService;
-                final IAccessibilityServiceClient proxyInterface = proxy.mServiceInterface;
+                final IBinder proxyBinder = proxy.mClientBinder;
+                final IAccessibilityServiceClient proxyInterface = proxy.mClient;
                 if ((proxyBinder != null) && (proxyInterface != null)) {
                     interfaces.add(proxyInterface);
                 }
diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
index f85d786..ed4eeb5 100644
--- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
@@ -107,8 +107,7 @@
                 Binder.getCallingUserHandle().getIdentifier());
         if (mUiAutomationService != null) {
             throw new IllegalStateException(
-                    "UiAutomationService " + mUiAutomationService.mServiceInterface
-                            + "already registered!");
+                    "UiAutomationService " + mUiAutomationService.mClient + "already registered!");
         }
 
         try {
@@ -130,10 +129,9 @@
                 mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal,
                 systemActionPerformer, awm);
         mUiAutomationServiceOwner = owner;
-        mUiAutomationService.mServiceInterface = serviceClient;
+        mUiAutomationService.mClient = serviceClient;
         try {
-            mUiAutomationService.mServiceInterface.asBinder().linkToDeath(mUiAutomationService,
-                    0);
+            mUiAutomationService.mClient.asBinder().linkToDeath(mUiAutomationService, 0);
         } catch (RemoteException re) {
             Slog.e(LOG_TAG, "Failed registering death link: " + re);
             destroyUiAutomationService();
@@ -149,10 +147,10 @@
         synchronized (mLock) {
             if (useAccessibility()
                     && ((mUiAutomationService == null)
-                    || (serviceClient == null)
-                    || (mUiAutomationService.mServiceInterface == null)
-                    || (serviceClient.asBinder()
-                    != mUiAutomationService.mServiceInterface.asBinder()))) {
+                            || (serviceClient == null)
+                            || (mUiAutomationService.mClient == null)
+                            || (serviceClient.asBinder()
+                                    != mUiAutomationService.mClient.asBinder()))) {
                 throw new IllegalStateException("UiAutomationService " + serviceClient
                         + " not registered!");
             }
@@ -230,8 +228,7 @@
     private void destroyUiAutomationService() {
         synchronized (mLock) {
             if (mUiAutomationService != null) {
-                mUiAutomationService.mServiceInterface.asBinder().unlinkToDeath(
-                        mUiAutomationService, 0);
+                mUiAutomationService.mClient.asBinder().unlinkToDeath(mUiAutomationService, 0);
                 mUiAutomationService.onRemoved();
                 mUiAutomationService.resetLocked();
                 mUiAutomationService = null;
@@ -271,40 +268,48 @@
 
         void connectServiceUnknownThread() {
             // This needs to be done on the main thread
-            mMainHandler.post(() -> {
-                try {
-                    final IAccessibilityServiceClient serviceInterface;
-                    final UiAutomationService uiAutomationService;
-                    synchronized (mLock) {
-                        serviceInterface = mServiceInterface;
-                        uiAutomationService = mUiAutomationService;
-                        if (serviceInterface == null) {
-                            mService = null;
-                        } else {
-                            mService = mServiceInterface.asBinder();
-                            mService.linkToDeath(this, 0);
+            mMainHandler.post(
+                    () -> {
+                        try {
+                            final IAccessibilityServiceClient client;
+                            final UiAutomationService uiAutomationService;
+                            synchronized (mLock) {
+                                client = mClient;
+                                uiAutomationService = mUiAutomationService;
+                                if (client == null) {
+                                    mClientBinder = null;
+                                } else {
+                                    mClientBinder = mClient.asBinder();
+                                    mClientBinder.linkToDeath(this, 0);
+                                }
+                            }
+                            // If the client is null, the UiAutomation has been shut down on
+                            // another thread.
+                            if (client != null && uiAutomationService != null) {
+                                uiAutomationService.addWindowTokensForAllDisplays();
+                                if (mTrace.isA11yTracingEnabledForTypes(
+                                        AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+                                    mTrace.logTrace(
+                                            "UiAutomationService.connectServiceUnknownThread",
+                                            AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT,
+                                            "serviceConnection="
+                                                    + this
+                                                    + ";connectionId="
+                                                    + mId
+                                                    + "windowToken="
+                                                    + mOverlayWindowTokens.get(
+                                                            Display.DEFAULT_DISPLAY));
+                                }
+                                client.init(
+                                        this,
+                                        mId,
+                                        mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+                            }
+                        } catch (RemoteException re) {
+                            Slog.w(LOG_TAG, "Error initializing connection", re);
+                            destroyUiAutomationService();
                         }
-                    }
-                    // If the serviceInterface is null, the UiAutomation has been shut down on
-                    // another thread.
-                    if (serviceInterface != null && uiAutomationService != null) {
-                        uiAutomationService.addWindowTokensForAllDisplays();
-                        if (mTrace.isA11yTracingEnabledForTypes(
-                                AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
-                            mTrace.logTrace("UiAutomationService.connectServiceUnknownThread",
-                                    AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT,
-                                    "serviceConnection=" + this + ";connectionId=" + mId
-                                    + "windowToken="
-                                    + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
-                        }
-                        serviceInterface.init(this, mId,
-                                mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
-                    }
-                } catch (RemoteException re) {
-                    Slog.w(LOG_TAG, "Error initializing connection", re);
-                    destroyUiAutomationService();
-                }
-            });
+                    });
         }
 
         @Override
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 1e723b5..082459b 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -21,11 +21,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
+import android.app.appfunctions.AppFunctionService;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.ExecuteAppFunctionResponse;
 import android.app.appfunctions.IAppFunctionManager;
 import android.app.appfunctions.IAppFunctionService;
+import android.app.appfunctions.ICancellationCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
 import android.app.appsearch.AppSearchManager;
@@ -37,11 +39,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
 import com.android.server.SystemService.TargetUser;
 import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
 import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
@@ -99,7 +105,7 @@
     }
 
     @Override
-    public void executeAppFunction(
+    public ICancellationSignal executeAppFunction(
             @NonNull ExecuteAppFunctionAidlRequest requestInternal,
             @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
         Objects.requireNonNull(requestInternal);
@@ -120,11 +126,14 @@
                             ExecuteAppFunctionResponse.RESULT_DENIED,
                             exception.getMessage(),
                             /* extras= */ null));
-            return;
+            return null;
         }
 
         int callingUid = Binder.getCallingUid();
-        int callingPid = Binder.getCallingUid();
+        int callingPid = Binder.getCallingPid();
+
+        ICancellationSignal localCancelTransport = CancellationSignal.createTransport();
+
         THREAD_POOL_EXECUTOR.execute(
                 () -> {
                     try {
@@ -132,12 +141,14 @@
                                 requestInternal,
                                 callingUid,
                                 callingPid,
+                                localCancelTransport,
                                 safeExecuteAppFunctionCallback);
                     } catch (Exception e) {
                         safeExecuteAppFunctionCallback.onResult(
                                 mapExceptionToExecuteAppFunctionResponse(e));
                     }
                 });
+        return localCancelTransport;
     }
 
     @WorkerThread
@@ -145,6 +156,7 @@
             ExecuteAppFunctionAidlRequest requestInternal,
             int callingUid,
             int callingPid,
+            ICancellationSignal localCancelTransport,
             SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
         UserHandle targetUser = requestInternal.getUserHandle();
         // TODO(b/354956319): Add and honor the new enterprise policies.
@@ -203,8 +215,10 @@
                                             requestInternal,
                                             serviceIntent,
                                             targetUser,
+                                            localCancelTransport,
                                             safeExecuteAppFunctionCallback,
-                                            /* bindFlags= */ Context.BIND_AUTO_CREATE);
+                                            /* bindFlags= */ Context.BIND_AUTO_CREATE
+                                                    | Context.BIND_FOREGROUND_SERVICE);
                                 })
                         .exceptionally(
                                 ex -> {
@@ -218,8 +232,19 @@
             @NonNull ExecuteAppFunctionAidlRequest requestInternal,
             @NonNull Intent serviceIntent,
             @NonNull UserHandle targetUser,
+            @NonNull ICancellationSignal cancellationSignalTransport,
             @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback,
             int bindFlags) {
+        CancellationSignal cancellationSignal =
+                CancellationSignal.fromTransport(cancellationSignalTransport);
+        ICancellationCallback cancellationCallback =
+                new ICancellationCallback.Stub() {
+                    @Override
+                    public void sendCancellationTransport(
+                            @NonNull ICancellationSignal cancellationTransport) {
+                        cancellationSignal.setRemote(cancellationTransport);
+                    }
+                };
         boolean bindServiceResult =
                 mRemoteServiceCaller.runServiceCall(
                         serviceIntent,
@@ -235,6 +260,7 @@
                                 try {
                                     service.executeAppFunction(
                                             requestInternal.getClientRequest(),
+                                            cancellationCallback,
                                             new IExecuteAppFunctionCallback.Stub() {
                                                 @Override
                                                 public void onResult(
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index b2c679f..f6ac706 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -1444,7 +1444,7 @@
      * in {@link #allocateAppWidgetId}.
      *
      * @param callingPackage The package that calls this method.
-     * @param appWidgetId The id of theapp widget to bind.
+     * @param appWidgetId The id of the widget to bind.
      * @param providerProfileId The user/profile id of the provider.
      * @param providerComponent The {@link ComponentName} that provides the widget.
      * @param options The options to pass to the provider.
@@ -1738,6 +1738,14 @@
         return false;
     }
 
+    /**
+     * Called by a {@link AppWidgetHost} to remove all records (i.e. {@link Host}
+     * and all {@link Widget} associated with the host) from a specified host.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param hostId id of the {@link Host}.
+     * @see AppWidgetHost#deleteHost()
+     */
     @Override
     public void deleteHost(String callingPackage, int hostId) {
         final int userId = UserHandle.getCallingUserId();
@@ -1771,6 +1779,15 @@
         }
     }
 
+    /**
+     * Called by a host process to remove all records (i.e. {@link Host}
+     * and all {@link Widget} associated with the host) from all hosts associated
+     * with the calling process.
+     *
+     * Typically used in clean up after test execution.
+     *
+     * @see AppWidgetHost#deleteAllHosts()
+     */
     @Override
     public void deleteAllHosts() {
         final int userId = UserHandle.getCallingUserId();
@@ -1805,6 +1822,18 @@
         }
     }
 
+    /**
+     * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget.
+     *
+     * Typically used by launcher during the restore of an AppWidget, the binding
+     * of new AppWidget, and during grid size migration.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetId   Id of the widget.
+     * @return The {@link AppWidgetProviderInfo} for the specified widget.
+     *
+     * @see AppWidgetManager#getAppWidgetInfo(int)
+     */
     @Override
     public AppWidgetProviderInfo getAppWidgetInfo(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
@@ -1859,6 +1888,17 @@
         }
     }
 
+    /**
+     * Returns the most recent {@link RemoteViews} of the specified AppWidget.
+     * Typically serves as a cache of the content of the AppWidget.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetId   Id of the widget.
+     * @return The {@link RemoteViews} of the specified widget.
+     *
+     * @see AppWidgetHost#updateAppWidgetDeferred(String, int)
+     * @see AppWidgetHost#setListener(int, AppWidgetHostListener)
+     */
     @Override
     public RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
@@ -1886,6 +1926,29 @@
         }
     }
 
+    /**
+     * Update the extras for a given widget instance.
+     * <p>
+     * The extras can be used to embed additional information about this widget to be accessed
+     * by the associated widget's AppWidgetProvider.
+     *
+     * <p>
+     * The new options are merged into existing options using {@link Bundle#putAll} semantics.
+     *
+     * <p>
+     * Typically called by a {@link AppWidgetHost} (e.g. Launcher) to notify
+     * {@link AppWidgetProvider} regarding contextual changes (e.g. sizes) when rendering the
+     * widget.
+     * Calling this method would trigger onAppWidgetOptionsChanged() callback on the provider's
+     * side.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetId Id of the widget.
+     * @param options New options associate with this widget.
+     *
+     * @see AppWidgetManager#getAppWidgetOptions(int, Bundle)
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
+     */
     @Override
     public void updateAppWidgetOptions(String callingPackage, int appWidgetId, Bundle options) {
         final int userId = UserHandle.getCallingUserId();
@@ -1919,6 +1982,21 @@
         }
     }
 
+    /**
+     * Get the extras associated with a given widget instance.
+     * <p>
+     * The extras can be used to embed additional information about this widget to be accessed
+     * by the associated widget's AppWidgetProvider.
+     *
+     * Typically called by a host process (e.g. Launcher) to determine if they need to update the
+     * options of the widget.
+     *
+     * @see #updateAppWidgetOptions(String, int, Bundle)
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetId Id of the widget.
+     * @return The options associated with the specified widget instance.
+     */
     @Override
     public Bundle getAppWidgetOptions(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
@@ -1946,6 +2024,28 @@
         }
     }
 
+    /**
+     * Updates the content of the widgets (as specified by appWidgetIds) using the provided
+     * {@link RemoteViews}.
+     *
+     * Typically called by the provider's process. Either in response to the invocation of
+     * {@link AppWidgetProvider#onUpdate} or upon receiving the
+     * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcast.
+     *
+     * <p>
+     * Note that the RemoteViews parameter will be cached by the AppWidgetService, and hence should
+     * contain a complete representation of the widget. For performing partial widget updates, see
+     * {@link #partiallyUpdateAppWidgetIds(String, int[], RemoteViews)}.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetIds Ids of the widgets to be updated.
+     * @param views The RemoteViews object containing the update.
+     *
+     * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[])
+     * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+     * @see AppWidgetManager#updateAppWidget(int, RemoteViews)
+     * @see AppWidgetManager#updateAppWidget(int[], RemoteViews)
+     */
     @Override
     public void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
             RemoteViews views) {
@@ -1956,6 +2056,27 @@
         updateAppWidgetIds(callingPackage, appWidgetIds, views, false);
     }
 
+    /**
+     * Perform an incremental update or command on the widget(s) specified by appWidgetIds.
+     * <p>
+     * This update  differs from {@link #updateAppWidgetIds(int[], RemoteViews)} in that the
+     * RemoteViews object which is passed is understood to be an incomplete representation of the
+     * widget, and hence does not replace the cached representation of the widget. As of API
+     * level 17, the new properties set within the views objects will be appended to the cached
+     * representation of the widget, and hence will persist.
+     *
+     * <p>
+     * This method will be ignored if a widget has not received a full update via
+     * {@link #updateAppWidget(int[], RemoteViews)}.
+     *
+     * @param callingPackage   The package that calls this method.
+     * @param appWidgetIds     Ids of the widgets to be updated.
+     * @param views            The RemoteViews object containing the incremental update / command.
+     *
+     * @see AppWidgetManager#partiallyUpdateAppWidget(int[], RemoteViews)
+     * @see RemoteViews#setDisplayedChild(int, int)
+     * @see RemoteViews#setScrollPosition(int, int)
+     */
     @Override
     public void partiallyUpdateAppWidgetIds(String callingPackage, int[] appWidgetIds,
             RemoteViews views) {
@@ -1966,6 +2087,24 @@
         updateAppWidgetIds(callingPackage, appWidgetIds, views, true);
     }
 
+    /**
+     * Callback function which marks specified providers as extended from AppWidgetProvider.
+     *
+     * This information is used to determine if the system can combine
+     * {@link AppWidgetManager#ACTION_APPWIDGET_ENABLED} and
+     * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} into a single broadcast.
+     *
+     * Note: The system can only combine the two broadcasts if the provider is extended from
+     * AppWidgetProvider. When they do, they are expected to override the
+     * {@link AppWidgetProvider#onUpdate} callback function to provide updates, as opposed to
+     * listening for {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcasts directly.
+     *
+     * @see AppWidgetManager#ACTION_APPWIDGET_ENABLED
+     * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+     * @see AppWidgetManager#ACTION_APPWIDGET_ENABLE_AND_UPDATE
+     * @see AppWidgetProvider#onReceive(Context, Intent)
+     * @see #sendEnableAndUpdateIntentLocked
+     */
     @Override
     public void notifyProviderInheritance(@Nullable final ComponentName[] componentNames) {
         final int userId = UserHandle.getCallingUserId();
@@ -2000,6 +2139,15 @@
         }
     }
 
+    /**
+     * Notifies the specified collection view in all the specified AppWidget instances
+     * to invalidate their data.
+     *
+     * This method is effectively deprecated since
+     * {@link RemoteViews#setRemoteAdapter(int, Intent)} has been deprecated.
+     *
+     * @see AppWidgetManager#notifyAppWidgetViewDataChanged(int[], int)
+     */
     @Override
     public void notifyAppWidgetViewDataChanged(String callingPackage, int[] appWidgetIds,
             int viewId) {
@@ -2035,6 +2183,18 @@
         }
     }
 
+    /**
+     * Updates the content of all widgets associated with given provider (as specified by
+     * componentName) using the provided {@link RemoteViews}.
+     *
+     * Typically called by the provider's process when there's an update that needs to be supplied
+     * to all instances of the widgets.
+     *
+     * @param componentName The component name of the provider.
+     * @param views The RemoteViews object containing the update.
+     *
+     * @see AppWidgetManager#updateAppWidget(ComponentName, RemoteViews)
+     */
     @Override
     public void updateAppWidgetProvider(ComponentName componentName, RemoteViews views) {
         final int userId = UserHandle.getCallingUserId();
@@ -2068,6 +2228,27 @@
         }
     }
 
+    /**
+     * Updates the info for the supplied AppWidget provider. Apps can use this to change the default
+     * behavior of the widget based on the state of the app (e.g., if the user is logged in
+     * or not). Calling this API completely replaces the previous definition.
+     *
+     * <p>
+     * The manifest entry of the provider should contain an additional meta-data tag similar to
+     * {@link AppWidgetManager#META_DATA_APPWIDGET_PROVIDER} which should point to any alternative
+     * definitions for the provider.
+     *
+     * <p>
+     * This is persisted across device reboots and app updates. If this meta-data key is not
+     * present in the manifest entry, the info reverts to default.
+     *
+     * @param provider {@link ComponentName} for the {@link
+     *    android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+     * @param metaDataKey key for the meta-data tag pointing to the new provider info. Use null
+     *    to reset any previously set info.
+     *
+     * @see AppWidgetManager#updateAppWidgetProviderInfo(ComponentName, String)
+     */
     @Override
     public void updateAppWidgetProviderInfo(ComponentName componentName, String metadataKey) {
         final int userId = UserHandle.getCallingUserId();
@@ -2119,6 +2300,11 @@
         }
     }
 
+    /**
+     * Returns true if the default launcher app on the device (the one that currently
+     * holds the android.app.role.HOME role) can support pinning widgets
+     * (typically means adding widgets into home screen).
+     */
     @Override
     public boolean isRequestPinAppWidgetSupported() {
         synchronized (mLock) {
@@ -2133,6 +2319,44 @@
                         LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET);
     }
 
+    /**
+     * Request to pin an app widget on the current launcher. It's up to the launcher to accept this
+     * request (optionally showing a user confirmation). If the request is accepted, the caller will
+     * get a confirmation with extra {@link #EXTRA_APPWIDGET_ID}.
+     *
+     * <p>When a request is denied by the user, the caller app will not get any response.
+     *
+     * <p>Only apps with a foreground activity or a foreground service can call it.  Otherwise
+     * it'll throw {@link IllegalStateException}.
+     *
+     * <p>It's up to the launcher how to handle previous pending requests when the same package
+     * calls this API multiple times in a row.  It may ignore the previous requests,
+     * for example.
+     *
+     * <p>Launcher will not show the configuration activity associated with the provider in this
+     * case. The app could either show the configuration activity as a response to the callback,
+     * or show if before calling the API (various configurations can be encapsulated in
+     * {@code successCallback} to avoid persisting them before the widgetId is known).
+     *
+     * @param provider The {@link ComponentName} for the {@link
+     *    android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+     * @param extras If not null, this is passed to the launcher app. For eg {@link
+     *    #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview.
+     * @param successCallback If not null, this intent will be sent when the widget is created.
+     *
+     * @return {@code TRUE} if the launcher supports this feature. Note the API will return without
+     *    waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean
+     *    the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature or if
+     *    calling app belongs to a user-profile with items restricted on home screen.
+     *
+     * @see android.content.pm.ShortcutManager#isRequestPinShortcutSupported()
+     * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender)
+     * @see AppWidgetManager#isRequestPinAppWidgetSupported()
+     * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent)
+     *
+     * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground
+     * service or when the user is locked.
+     */
     @Override
     public boolean requestPinAppWidget(String callingPackage, ComponentName componentName,
             Bundle extras, IntentSender resultSender) {
@@ -2184,6 +2408,24 @@
                 callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
     }
 
+    /**
+     * Gets the AppWidget providers for the given user profile. User profile can only
+     * be the current user or a profile of the current user. For example, the current
+     * user may have a corporate profile. In this case the parent user profile has a
+     * child profile, the corporate one.
+     *
+     * @param categoryFilter Will only return providers which register as any of the specified
+     *        specified categories. See {@link AppWidgetProviderInfo#widgetCategory}.
+     * @param profile A profile of the current user which to be queried. The user
+     *        is itself also a profile. If null, the providers only for the current user
+     *        are returned.
+     * @param packageName If specified, will only return providers from the given package.
+     * @return The installed providers.
+     *
+     * @see android.os.Process#myUserHandle()
+     * @see android.os.UserManager#getUserProfiles()
+     * @see AppWidgetManager#getInstalledProvidersForProfile(int, UserHandle, String)
+     */
     @Override
     public ParceledListSlice<AppWidgetProviderInfo> getInstalledProvidersForProfile(int categoryFilter,
             int profileId, String packageName) {
@@ -2244,6 +2486,26 @@
         }
     }
 
+    /**
+     * Updates the content of the widgets (as specified by appWidgetIds) using the provided
+     * {@link RemoteViews}.
+     *
+     * If performing a partial update, the given RemoteViews object is merged into existing
+     * RemoteViews object.
+     *
+     * Fails silently if appWidgetIds is null or empty, or cannot found a widget with the given
+     * appWidgetId.
+     *
+     * @param callingPackage The package that calls this method.
+     * @param appWidgetIds Ids of the widgets to be updated.
+     * @param views The RemoteViews object containing the update.
+     * @param partially Whether it was a partial update.
+     *
+     * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[])
+     * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+     * @see AppWidgetManager#updateAppWidget(int, RemoteViews)
+     * @see AppWidgetManager#updateAppWidget(int[], RemoteViews)
+     */
     private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
             RemoteViews views, boolean partially) {
         final int userId = UserHandle.getCallingUserId();
@@ -2273,12 +2535,29 @@
         }
     }
 
+    /**
+     * Increment the counter of widget ids and return the new id.
+     *
+     * Typically called by {@link #allocateAppWidgetId} when a instance of widget is created,
+     * either as a result of being pinned by launcher or added during a restore.
+     *
+     * Note: A widget id is a monotonically increasing integer that uniquely identifies the widget
+     * instance.
+     *
+     * TODO: Revisit this method and determine whether we need to alter the widget id during
+     *       the restore since widget id mismatch potentially leads to some issues in the past.
+     */
     private int incrementAndGetAppWidgetIdLocked(int userId) {
         final int appWidgetId = peekNextAppWidgetIdLocked(userId) + 1;
         mNextAppWidgetIds.put(userId, appWidgetId);
         return appWidgetId;
     }
 
+    /**
+     * Called by {@link #readProfileStateFromFileLocked} when widgets/providers/hosts are loaded
+     * from disk, which ensures mNextAppWidgetIds is larger than any existing widget id for given
+     * user.
+     */
     private void setMinAppWidgetIdLocked(int userId, int minWidgetId) {
         final int nextAppWidgetId = peekNextAppWidgetIdLocked(userId);
         if (nextAppWidgetId < minWidgetId) {
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 0713999..60b826b 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -41,6 +41,7 @@
     public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3;
     public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4;
     public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5;
+    public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6;
 
     /** @hide */
     @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
@@ -50,6 +51,7 @@
             CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
             CPU_WAKEUP_SUBSYSTEM_SENSOR,
             CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+            CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CpuWakeupSubsystem {
@@ -99,6 +101,14 @@
     public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid);
 
     /**
+     * Informs battery stats of a sysproxy packet that woke up the CPU
+     *
+     * @param uid The uid that received the packet.
+     * @param elapsedMillis The time of the packet's arrival in elapsed timebase.
+     */
+    public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis);
+
+    /**
      * Informs battery stats of binder stats for the given work source UID.
      */
     public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount,
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index cadceb5..a6e21fc 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -659,7 +659,7 @@
             @Override
             public void run() {
                 try {
-                    RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
+                    RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage);
                 } catch (Throwable t) {
                     logRescueException(level, failedPackage, t);
                 }
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index d86bae1..e64a480 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -219,7 +219,7 @@
     public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10;
 
     /** Extended timeout for the system server watchdog. */
-    private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000;
+    private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000;
 
     /** Extended timeout for the system server watchdog for vold#partition operation. */
     private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000;
@@ -3251,7 +3251,7 @@
         if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
             throw new SecurityException("no permission to commit checkpoint changes");
         }
-
+        extendWatchdogTimeout("vold#commitChanges might be slow");
         mVold.commitChanges();
     }
 
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 89dfc73..12e8c57 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -380,10 +380,12 @@
         }
 
         /** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+        // TODO: b/262269892 This method was created to perform experiments before the relevant API
+        // was exposed. Now it is obsolete and should be removed.
         @VisibleForTesting(visibility = Visibility.PRIVATE)
         public Set<Integer> getRestrictedTransportsFromCarrierConfig(
                 ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
-            if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
+            if (!Build.isDebuggable()) {
                 return RESTRICTED_TRANSPORTS_DEFAULT;
             }
 
diff --git a/services/core/java/com/android/server/WiredAccessoryManager.java b/services/core/java/com/android/server/WiredAccessoryManager.java
index b271d7e..ab69cd1 100644
--- a/services/core/java/com/android/server/WiredAccessoryManager.java
+++ b/services/core/java/com/android/server/WiredAccessoryManager.java
@@ -118,8 +118,11 @@
             if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_LINEOUT_INSERT) == 1) {
                 switchValues |= SW_LINEOUT_INSERT_BIT;
             }
-            notifyWiredAccessoryChanged(0, switchValues,
-                    SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT);
+            notifyWiredAccessoryChanged(
+                    0,
+                    switchValues,
+                    SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT,
+                    true /*isSynchronous*/);
         }
 
 
@@ -135,7 +138,13 @@
     }
 
     @Override
-    public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask) {
+    public void notifyWiredAccessoryChanged(
+            long whenNanos, int switchValues, int switchMask) {
+        notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask, false /*isSynchronous*/);
+    }
+
+    public void notifyWiredAccessoryChanged(
+            long whenNanos, int switchValues, int switchMask, boolean isSynchronous) {
         if (LOG) {
             Slog.v(TAG, "notifyWiredAccessoryChanged: when=" + whenNanos
                     + " bits=" + switchCodeToString(switchValues, switchMask)
@@ -172,8 +181,10 @@
                     break;
             }
 
-            updateLocked(NAME_H2W,
-                    (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
+            updateLocked(
+                    NAME_H2W,
+                    (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset,
+                    isSynchronous);
         }
     }
 
@@ -195,8 +206,9 @@
      *
      * @param newName  One of the NAME_xxx variables defined above.
      * @param newState 0 or one of the BIT_xxx variables defined above.
+     * @param isSynchronous boolean to determine whether should happen sync or async
      */
-    private void updateLocked(String newName, int newState) {
+    private void updateLocked(String newName, int newState, boolean isSynchronous) {
         // Retain only relevant bits
         int headsetState = newState & SUPPORTED_HEADSETS;
         int usb_headset_anlg = headsetState & BIT_USB_HEADSET_ANLG;
@@ -234,12 +246,15 @@
             return;
         }
 
-        mWakeLock.acquire();
-
-        Log.i(TAG, "MSG_NEW_DEVICE_STATE");
-        Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
-                mHeadsetState, "");
-        mHandler.sendMessage(msg);
+        if (isSynchronous) {
+            setDevicesState(headsetState, mHeadsetState, "");
+        } else {
+            mWakeLock.acquire();
+            Log.i(TAG, "MSG_NEW_DEVICE_STATE");
+            Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
+                    mHeadsetState, "");
+            mHandler.sendMessage(msg);
+        }
 
         mHeadsetState = headsetState;
     }
@@ -439,7 +454,10 @@
             for (int i = 0; i < mUEventInfo.size(); ++i) {
                 UEventInfo uei = mUEventInfo.get(i);
                 if (devPath.equals(uei.getDevPath())) {
-                    updateLocked(name, uei.computeNewHeadsetState(mHeadsetState, state));
+                    updateLocked(
+                            name,
+                            uei.computeNewHeadsetState(mHeadsetState, state),
+                            false /*isSynchronous*/);
                     return;
                 }
             }
@@ -550,7 +568,10 @@
             synchronized (mLock) {
                 int mask = maskAndState.first;
                 int state = maskAndState.second;
-                updateLocked(name, mHeadsetState & ~(mask & ~state) | (mask & state));
+                updateLocked(
+                        name,
+                        mHeadsetState & ~(mask & ~state) | (mask & state),
+                        false /*isSynchronous*/);
                 return;
             }
         }
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 88edb12..3499a3a 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -1773,8 +1773,7 @@
                         // Create a Session for the target user and pass in the bundle
                         completeCloningAccount(response, result, account, toAccounts, userFrom);
                     } else {
-                        // Bundle format is not defined.
-                        super.onResultSkipSanitization(result);
+                        super.onResult(result);
                     }
                 }
             }.bind();
@@ -1861,8 +1860,7 @@
                     // account to avoid retries?
                     // TODO: what we do with the visibility?
 
-                    // Bundle format is not defined.
-                    super.onResultSkipSanitization(result);
+                    super.onResult(result);
                 }
 
                 @Override
@@ -2108,7 +2106,6 @@
         @Override
         public void onResult(Bundle result) {
             Bundle.setDefusable(result, true);
-            result = sanitizeBundle(result);
             IAccountManagerResponse response = getResponseAndClose();
             if (response != null) {
                 try {
@@ -2462,7 +2459,6 @@
         @Override
         public void onResult(Bundle result) {
             Bundle.setDefusable(result, true);
-            result = sanitizeBundle(result);
             if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
                     && !result.containsKey(AccountManager.KEY_INTENT)) {
                 final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
@@ -2977,7 +2973,6 @@
                 @Override
                 public void onResult(Bundle result) {
                     Bundle.setDefusable(result, true);
-                    result = sanitizeBundle(result);
                     if (result != null) {
                         String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL);
                         Bundle bundle = new Bundle();
@@ -3155,7 +3150,6 @@
                 @Override
                 public void onResult(Bundle result) {
                     Bundle.setDefusable(result, true);
-                    result = sanitizeBundle(result);
                     if (result != null) {
                         if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) {
                             Intent intent = newGrantCredentialsPermissionIntent(
@@ -3627,12 +3621,6 @@
         @Override
         public void onResult(Bundle result) {
             Bundle.setDefusable(result, true);
-            Bundle sessionBundle = null;
-            if (result != null) {
-                // Session bundle will be removed from result.
-                sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
-            }
-            result = sanitizeBundle(result);
             mNumResults++;
             Intent intent = null;
             if (result != null) {
@@ -3694,6 +3682,7 @@
             // bundle contains data necessary for finishing the session
             // later. The session bundle will be encrypted here and
             // decrypted later when trying to finish the session.
+            Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
             if (sessionBundle != null) {
                 String accountType = sessionBundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
                 if (TextUtils.isEmpty(accountType)
@@ -4081,7 +4070,6 @@
                 @Override
                 public void onResult(Bundle result) {
                     Bundle.setDefusable(result, true);
-                    result = sanitizeBundle(result);
                     IAccountManagerResponse response = getResponseAndClose();
                     if (response == null) {
                         return;
@@ -4395,7 +4383,6 @@
         @Override
         public void onResult(Bundle result) {
             Bundle.setDefusable(result, true);
-            result = sanitizeBundle(result);
             mNumResults++;
             if (result == null) {
                 onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
@@ -4952,68 +4939,6 @@
                 callback, resultReceiver);
     }
 
-
-    // All keys for Strings passed from AbstractAccountAuthenticator using Bundle.
-    private static final String[] sStringBundleKeys = new String[] {
-        AccountManager.KEY_ACCOUNT_NAME,
-        AccountManager.KEY_ACCOUNT_TYPE,
-        AccountManager.KEY_AUTHTOKEN,
-        AccountManager.KEY_AUTH_TOKEN_LABEL,
-        AccountManager.KEY_ERROR_MESSAGE,
-        AccountManager.KEY_PASSWORD,
-        AccountManager.KEY_ACCOUNT_STATUS_TOKEN};
-
-    /**
-     * Keep only documented fields in a Bundle received from AbstractAccountAuthenticator.
-     */
-    protected static Bundle sanitizeBundle(Bundle bundle) {
-        if (bundle == null) {
-            return null;
-        }
-        Bundle sanitizedBundle = new Bundle();
-        Bundle.setDefusable(sanitizedBundle, true);
-        int updatedKeysCount = 0;
-        for (String stringKey : sStringBundleKeys) {
-            if (bundle.containsKey(stringKey)) {
-                String value = bundle.getString(stringKey);
-                sanitizedBundle.putString(stringKey, value);
-                updatedKeysCount++;
-            }
-        }
-        String key = AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY;
-        if (bundle.containsKey(key)) {
-            long expiryMillis = bundle.getLong(key, 0L);
-            sanitizedBundle.putLong(key, expiryMillis);
-            updatedKeysCount++;
-        }
-        key = AccountManager.KEY_BOOLEAN_RESULT;
-        if (bundle.containsKey(key)) {
-            boolean booleanResult = bundle.getBoolean(key, false);
-            sanitizedBundle.putBoolean(key, booleanResult);
-            updatedKeysCount++;
-        }
-        key = AccountManager.KEY_ERROR_CODE;
-        if (bundle.containsKey(key)) {
-            int errorCode = bundle.getInt(key, 0);
-            sanitizedBundle.putInt(key, errorCode);
-            updatedKeysCount++;
-        }
-        key = AccountManager.KEY_INTENT;
-        if (bundle.containsKey(key)) {
-            Intent intent = bundle.getParcelable(key, Intent.class);
-            sanitizedBundle.putParcelable(key, intent);
-            updatedKeysCount++;
-        }
-        if (bundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE)) {
-            // The field is not copied in sanitized bundle.
-            updatedKeysCount++;
-        }
-        if (updatedKeysCount != bundle.size()) {
-            Log.w(TAG, "Size mismatch after sanitizeBundle call.");
-        }
-        return sanitizedBundle;
-    }
-
     private abstract class Session extends IAccountAuthenticatorResponse.Stub
             implements IBinder.DeathRecipient, ServiceConnection {
         private final Object mSessionLock = new Object();
@@ -5304,15 +5229,10 @@
                 }
             }
         }
+
         @Override
         public void onResult(Bundle result) {
             Bundle.setDefusable(result, true);
-            result = sanitizeBundle(result);
-            onResultSkipSanitization(result);
-        }
-
-        public void onResultSkipSanitization(Bundle result) {
-            Bundle.setDefusable(result, true);
             mNumResults++;
             Intent intent = null;
             if (result != null) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a85e2a2..35323d6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -398,6 +398,7 @@
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener;
 import com.android.internal.os.BinderCallHeavyHitterWatcher.HeavyHitterContainer;
@@ -578,7 +579,7 @@
     static final int RESERVED_BYTES_PER_LOGCAT_LINE = 100;
 
     // How many seconds should the system wait before terminating the spawned logcat process.
-    static final int LOGCAT_TIMEOUT_SEC = 10;
+    static final int LOGCAT_TIMEOUT_SEC = Flags.logcatLongerTimeout() ? 15 : 10;
 
     // Necessary ApplicationInfo flags to mark an app as persistent
     static final int PERSISTENT_MASK =
@@ -839,6 +840,8 @@
     @GuardedBy("this")
     final ComponentAliasResolver mComponentAliasResolver;
 
+    final FileDescriptor mApplicationSharedMemoryReadOnlyFd;
+
     private static final long HOME_LAUNCH_TIMEOUT_MS = 15000;
     private final AtomicBoolean mHasHomeDelay = new AtomicBoolean(false);
 
@@ -2416,6 +2419,7 @@
         mBroadcastQueue = injector.getBroadcastQueue(this);
         mBroadcastController = new BroadcastController(mContext, this, mBroadcastQueue);
         mComponentAliasResolver = new ComponentAliasResolver(this);
+        mApplicationSharedMemoryReadOnlyFd = null;
     }
 
     // Note: This method is invoked on the main thread but may need to attach various
@@ -2522,6 +2526,13 @@
         mPendingStartActivityUids = new PendingStartActivityUids();
         mTraceErrorLogger = new TraceErrorLogger();
         mComponentAliasResolver = new ComponentAliasResolver(this);
+        try {
+            mApplicationSharedMemoryReadOnlyFd =
+                    ApplicationSharedMemory.getInstance().getReadOnlyFileDescriptor();
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to get read only fd for shared memory", e);
+            throw new RuntimeException(e);
+        }
     }
 
     void setBroadcastQueueForTest(BroadcastQueue broadcastQueue) {
@@ -4728,6 +4739,7 @@
                         app.getDisabledCompatChanges(),
                         app.getLoggableCompatChanges(),
                         serializedSystemFontMap,
+                        mApplicationSharedMemoryReadOnlyFd,
                         app.getStartElapsedTime(),
                         app.getStartUptime());
             }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 75e9fad..ef82c74 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -437,7 +437,6 @@
         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
                 mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
                 mPowerStatsStore, Clock.SYSTEM_CLOCK);
-        mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore);
         mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
         mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
         mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -504,6 +503,9 @@
     }
 
     public void systemServicesReady() {
+        mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
+                Flags.accumulateBatteryUsageStats());
+
         MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
                 Flags.streamlinedBatteryStats());
@@ -662,6 +664,8 @@
             } else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
                 return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
             }
+            // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups.
+            // See noteCpuWakingSysproxyPacket method.
             return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
         }
 
@@ -684,6 +688,15 @@
         }
 
         @Override
+        public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) {
+            if (uid < 0) {
+                Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid);
+                return;
+            }
+            noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid);
+        }
+
+        @Override
         public void noteBinderCallStats(int workSourceUid, long incrementatCallCount,
                 Collection<BinderCallsStats.CallStat> callStats) {
             synchronized (BatteryStatsService.this.mLock) {
@@ -1100,14 +1113,17 @@
                                     DEVICE_CONFIG_NAMESPACE,
                                     MIN_CONSUMED_POWER_THRESHOLD_KEY,
                                     0);
-                    final BatteryUsageStatsQuery query =
-                            new BatteryUsageStatsQuery.Builder()
-                                    .setMaxStatsAgeMs(0)
-                                    .includeProcessStateData()
-                                    .includeVirtualUids()
-                                    .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
-                                    .build();
-                    bus = getBatteryUsageStats(List.of(query)).get(0);
+                    BatteryUsageStatsQuery.Builder query = new BatteryUsageStatsQuery.Builder()
+                            .setMaxStatsAgeMs(0)
+                            .includeProcessStateData()
+                            .includeVirtualUids()
+                            .setMinConsumedPowerThreshold(minConsumedPowerThreshold);
+
+                    if (Flags.accumulateBatteryUsageStats()) {
+                        query.accumulated();
+                    }
+
+                    bus = getBatteryUsageStats(List.of(query.build())).get(0);
                     final int pullResult =
                             new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
                     try {
@@ -3016,6 +3032,9 @@
         if (Flags.streamlinedBatteryStats()) {
             pw.println("  --sample: collect and dump a sample of stats for debugging purpose");
         }
+        if (Flags.accumulateBatteryUsageStats()) {
+            pw.println("  --accumulated: continuously accumulated since setup or reset-all");
+        }
         pw.println("  <package.name>: optional name of package to filter output by.");
         pw.println("  -h: print this help text.");
         pw.println("Battery stats (batterystats) commands:");
@@ -3083,7 +3102,7 @@
     }
 
     private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model,
-            boolean proto) {
+            boolean proto, boolean accumulated) {
         awaitCompletion();
         syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
 
@@ -3097,6 +3116,9 @@
         if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
             builder.powerProfileModeledOnly();
         }
+        if (accumulated) {
+            builder.accumulated();
+        }
         BatteryUsageStatsQuery query = builder.build();
         synchronized (mStats) {
             mStats.prepareForDumpLocked();
@@ -3287,6 +3309,7 @@
                 } else if ("--usage".equals(arg)) {
                     int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
                     boolean proto = false;
+                    boolean accumulated = false;
                     for (int j = i + 1; j < args.length; j++) {
                         switch (args[j]) {
                             case "--proto":
@@ -3309,9 +3332,12 @@
                                 }
                                 break;
                             }
+                            case "--accumulated":
+                                accumulated = true;
+                                break;
                         }
                     }
-                    dumpUsageStats(fd, pw, model, proto);
+                    dumpUsageStats(fd, pw, model, proto, accumulated);
                     return;
                 } else if ("--wakeups".equals(arg)) {
                     mCpuWakeupStats.dump(new IndentingPrintWriter(pw, "  "),
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index a13ce65..bae9a67 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -40,6 +40,7 @@
 import android.aconfigd.Aconfigd.StorageReturnMessage;
 import android.aconfigd.Aconfigd.StorageReturnMessages;
 import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
+import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -491,14 +492,18 @@
     static void writeFlagOverrideRequest(
         ProtoOutputStream proto, String packageName, String flagName, String flagValue,
         boolean isLocal) {
+      int localOverrideTag = supportImmediateLocalOverrides()
+          ? StorageRequestMessage.LOCAL_IMMEDIATE
+          : StorageRequestMessage.LOCAL_ON_REBOOT;
+
       long msgsToken = proto.start(StorageRequestMessages.MSGS);
       long msgToken = proto.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE);
       proto.write(StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, packageName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flagName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flagValue);
       proto.write(StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, isLocal
-                ? StorageRequestMessage.LOCAL_ON_REBOOT
-                : StorageRequestMessage.SERVER_ON_REBOOT);
+            ? localOverrideTag
+            : StorageRequestMessage.SERVER_ON_REBOOT);
       proto.end(msgToken);
       proto.end(msgsToken);
     }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 3334393..4f6da3b 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -194,4 +194,23 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+
+flag {
+    name: "logcat_longer_timeout"
+    namespace: "backstage_power"
+    description: "Wait longer during the logcat gathering operation"
+    bug: "292533246"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "defer_display_events_when_frozen"
+    namespace: "system_performance"
+    is_fixed_read_only: true
+    description: "Defer submitting display events to frozen processes."
+    bug: "326315985"
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a6389f7..e0cf96f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1031,6 +1031,9 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
             String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern();
             int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0fd22c5..87504154 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1321,9 +1321,9 @@
         sendLMsgNoDelay(MSG_II_SET_LE_AUDIO_OUT_VOLUME, SENDMSG_REPLACE, info);
     }
 
-    /*package*/ void postSetModeOwner(int mode, int pid, int uid) {
-        sendLMsgNoDelay(MSG_I_SET_MODE_OWNER, SENDMSG_REPLACE,
-                new AudioModeInfo(mode, pid, uid));
+    /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) {
+        sendILMsgNoDelay(MSG_IL_SET_MODE_OWNER, SENDMSG_REPLACE,
+                signal ? 1 : 0, new AudioModeInfo(mode, pid, uid));
     }
 
     /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) {
@@ -1564,38 +1564,6 @@
         sendLMsgNoDelay(MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED, SENDMSG_QUEUE, client);
     }
 
-    /*package*/ void postSaveSetPreferredDevicesForStrategy(int strategy,
-                                                            List<AudioDeviceAttributes> devices)
-    {
-        sendILMsgNoDelay(MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy, devices);
-    }
-
-    /*package*/ void postSaveRemovePreferredDevicesForStrategy(int strategy) {
-        sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
-    }
-
-    /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy(
-            int strategy, AudioDeviceAttributes device) {
-        sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
-    }
-
-    /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy(
-            int strategy, AudioDeviceAttributes device) {
-        sendILMsgNoDelay(
-                MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
-    }
-
-    /*package*/ void postSaveSetPreferredDevicesForCapturePreset(
-            int capturePreset, List<AudioDeviceAttributes> devices) {
-        sendILMsgNoDelay(
-                MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset, devices);
-    }
-
-    /*package*/ void postSaveClearPreferredDevicesForCapturePreset(int capturePreset) {
-        sendIMsgNoDelay(
-                MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset);
-    }
-
     /*package*/ void postUpdateCommunicationRouteClient(
             int btScoRequesterUid, String eventSource) {
         sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE,
@@ -2025,7 +1993,7 @@
                         mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
                     }
                     break;
-                case MSG_I_SET_MODE_OWNER:
+                case MSG_IL_SET_MODE_OWNER:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
                             int btScoRequesterUid = bluetoothScoRequestOwnerUid();
@@ -2036,6 +2004,9 @@
                             }
                         }
                     }
+                    if (msg.arg1 == 1 /*signal*/) {
+                        mAudioService.decrementAudioModeResetCount();
+                    }
                     break;
 
                 case MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT:
@@ -2109,40 +2080,9 @@
                         mDeviceInventory.setBluetoothActiveDevice(btInfo);
                     }
                 } break;
-                case MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    final List<AudioDeviceAttributes> devices =
-                            (List<AudioDeviceAttributes>) msg.obj;
-                    mDeviceInventory.onSaveSetPreferredDevices(strategy, devices);
-                } break;
-                case MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    mDeviceInventory.onSaveRemovePreferredDevices(strategy);
-                } break;
-                case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
-                    mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device);
-                } break;
-                case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
-                    mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device);
-                } break;
                 case MSG_CHECK_MUTE_MUSIC:
                     checkMessagesMuteMusic(0);
                     break;
-                case MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET: {
-                    final int capturePreset = msg.arg1;
-                    final List<AudioDeviceAttributes> devices =
-                            (List<AudioDeviceAttributes>) msg.obj;
-                    mDeviceInventory.onSaveSetPreferredDevicesForCapturePreset(
-                            capturePreset, devices);
-                } break;
-                case MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET: {
-                    final int capturePreset = msg.arg1;
-                    mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
-                } break;
                 case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: {
                     final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
                     BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
@@ -2224,7 +2164,7 @@
     private static final int MSG_REPORT_NEW_ROUTES = 13;
     private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
     private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
-    private static final int MSG_I_SET_MODE_OWNER = 16;
+    private static final int MSG_IL_SET_MODE_OWNER = 16;
 
     private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22;
     private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23;
@@ -2235,16 +2175,10 @@
     // process external command to (dis)connect a hearing aid device
     private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
 
-    private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY = 32;
-    private static final int MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY = 33;
-
     private static final int MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED = 34;
     private static final int MSG_CHECK_MUTE_MUSIC = 35;
     private static final int MSG_REPORT_NEW_ROUTES_A2DP = 36;
 
-    private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 37;
-    private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
-
     private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42;
     private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
 
@@ -2253,8 +2187,6 @@
     // process set volume for Le Audio, obj is BleVolumeInfo
     private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
 
-    private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
-    private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
     private static final int MSG_IIL_BTLEAUDIO_TIMEOUT = 49;
 
     private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index a9bff8b..5fd12c2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -548,14 +548,17 @@
     @GuardedBy("mDevicesLock")
     private final ArrayMap<Integer, String> mApmConnectedDevices = new ArrayMap<>();
 
+    @GuardedBy("mDevicesLock")
     // List of preferred devices for strategies
     private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices =
             new ArrayMap<>();
 
+    @GuardedBy("mDevicesLock")
     // List of non-default devices for strategies
     private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices =
             new ArrayMap<>();
 
+    @GuardedBy("mDevicesLock")
     // List of preferred devices of capture preset
     private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset =
             new ArrayMap<>();
@@ -808,24 +811,18 @@
         synchronized (mDevicesLock) {
             mAppliedStrategyRoles.clear();
             mAppliedPresetRoles.clear();
-        }
-        synchronized (mPreferredDevices) {
             mPreferredDevices.forEach((strategy, devices) -> {
                 setPreferredDevicesForStrategy(strategy, devices);
             });
-        }
-        synchronized (mNonDefaultDevices) {
             mNonDefaultDevices.forEach((strategy, devices) -> {
                 addDevicesRoleForStrategy(strategy, AudioSystem.DEVICE_ROLE_DISABLED,
                         devices, false /* internal */);
             });
-        }
-        synchronized (mPreferredDevicesForCapturePreset) {
             mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
                 setDevicesRoleForCapturePreset(
                         capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
             });
-        }
+       }
     }
 
     /** only public for mocking/spying, do not call outside of AudioService */
@@ -1225,7 +1222,8 @@
         mmi.record();
     }
 
-    /*package*/ void onSaveSetPreferredDevices(int strategy,
+    @GuardedBy("mDevicesLock")
+    private void saveSetPreferredDevices(int strategy,
                                                @NonNull List<AudioDeviceAttributes> devices) {
         mPreferredDevices.put(strategy, devices);
         List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
@@ -1243,12 +1241,14 @@
         dispatchPreferredDevice(strategy, devices);
     }
 
-    /*package*/ void onSaveRemovePreferredDevices(int strategy) {
+    @GuardedBy("mDevicesLock")
+    private void saveRemovePreferredDevices(int strategy) {
         mPreferredDevices.remove(strategy);
         dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>());
     }
 
-    /*package*/ void onSaveSetDeviceAsNonDefault(int strategy,
+    @GuardedBy("mDevicesLock")
+    private void saveSetDeviceAsNonDefault(int strategy,
                                                  @NonNull AudioDeviceAttributes device) {
         List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
         if (nonDefaultDevices == null) {
@@ -1272,7 +1272,8 @@
         }
     }
 
-    /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy,
+    @GuardedBy("mDevicesLock")
+    private void saveRemoveDeviceAsNonDefault(int strategy,
                                                     @NonNull AudioDeviceAttributes device) {
         List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
         if (nonDefaultDevices != null) {
@@ -1282,14 +1283,16 @@
         }
     }
 
-    /*package*/ void onSaveSetPreferredDevicesForCapturePreset(
+    @GuardedBy("mDevicesLock")
+    private void saveSetPreferredDevicesForCapturePreset(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
         mPreferredDevicesForCapturePreset.put(capturePreset, devices);
         dispatchDevicesRoleForCapturePreset(
                 capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
     }
 
-    /*package*/ void onSaveClearPreferredDevicesForCapturePreset(int capturePreset) {
+    @GuardedBy("mDevicesLock")
+    private void saveClearPreferredDevicesForCapturePreset(int capturePreset) {
         mPreferredDevicesForCapturePreset.remove(capturePreset);
         dispatchDevicesRoleForCapturePreset(
                 capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED,
@@ -1301,21 +1304,22 @@
 
     /*package*/ int setPreferredDevicesForStrategyAndSave(int strategy,
             @NonNull List<AudioDeviceAttributes> devices) {
-        final int status = setPreferredDevicesForStrategy(strategy, devices);
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                final int status = setPreferredDevicesForStrategy(strategy, devices);
+                if (status == AudioSystem.SUCCESS) {
+                    saveSetPreferredDevices(strategy, devices);
+                }
+                return status;
+            }
         }
-        return status;
     }
     // Only used for external requests coming from an API
     /*package*/ int setPreferredDevicesForStrategy(int strategy,
             @NonNull List<AudioDeviceAttributes> devices) {
-        int status = AudioSystem.ERROR;
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = setDevicesRoleForStrategy(
-                    strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */);
-        }
-        return status;
+
+        return setDevicesRoleForStrategy(
+                strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */);
     }
     // Only used for internal requests
     /*package*/ int setPreferredDevicesForStrategyInt(int strategy,
@@ -1326,21 +1330,21 @@
     }
 
     /*package*/ int removePreferredDevicesForStrategyAndSave(int strategy) {
-        final int status = removePreferredDevicesForStrategy(strategy);
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                final int status = removePreferredDevicesForStrategy(strategy);
+                if (status == AudioSystem.SUCCESS) {
+                    saveRemovePreferredDevices(strategy);
+                }
+                return status;
+            }
         }
-        return status;
     }
     // Only used for external requests coming from an API
     /*package*/ int removePreferredDevicesForStrategy(int strategy) {
-        int status = AudioSystem.ERROR;
 
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = clearDevicesRoleForStrategy(
+        return clearDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_PREFERRED, false /*internal */);
-        }
-        return status;
     }
     // Only used for internal requests
     /*package*/ int removePreferredDevicesForStrategyInt(int strategy) {
@@ -1351,16 +1355,17 @@
     /*package*/ int setDeviceAsNonDefaultForStrategyAndSave(int strategy,
             @NonNull AudioDeviceAttributes device) {
         int status = AudioSystem.ERROR;
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            List<AudioDeviceAttributes> devices = new ArrayList<>();
-            devices.add(device);
-            status = addDevicesRoleForStrategy(
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                List<AudioDeviceAttributes> devices = new ArrayList<>();
+                devices.add(device);
+                status = addDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
-        }
 
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device);
+                if (status == AudioSystem.SUCCESS) {
+                    saveSetDeviceAsNonDefault(strategy, device);
+                }
+            }
         }
         return status;
     }
@@ -1368,16 +1373,17 @@
     /*package*/ int removeDeviceAsNonDefaultForStrategyAndSave(int strategy,
             @NonNull AudioDeviceAttributes device) {
         int status = AudioSystem.ERROR;
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            List<AudioDeviceAttributes> devices = new ArrayList<>();
-            devices.add(device);
-            status = removeDevicesRoleForStrategy(
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                List<AudioDeviceAttributes> devices = new ArrayList<>();
+                devices.add(device);
+                status = removeDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
-        }
 
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device);
+                if (status == AudioSystem.SUCCESS) {
+                    saveRemoveDeviceAsNonDefault(strategy, device);
+                }
+            }
         }
         return status;
     }
@@ -1405,41 +1411,40 @@
 
     /*package*/ int setPreferredDevicesForCapturePresetAndSave(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
-        final int status = setPreferredDevicesForCapturePreset(capturePreset, devices);
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                final int status = setPreferredDevicesForCapturePreset(capturePreset, devices);
+                if (status == AudioSystem.SUCCESS) {
+                    saveSetPreferredDevicesForCapturePreset(capturePreset, devices);
+                }
+                return status;
+            }
         }
-        return status;
     }
 
     // Only used for external requests coming from an API
     private int setPreferredDevicesForCapturePreset(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
-        int status = AudioSystem.ERROR;
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = setDevicesRoleForCapturePreset(
+        return setDevicesRoleForCapturePreset(
                     capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
-        }
-        return status;
     }
 
     /*package*/ int clearPreferredDevicesForCapturePresetAndSave(int capturePreset) {
-        final int status  = clearPreferredDevicesForCapturePreset(capturePreset);
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
+        synchronized(mDevicesLock){
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                final int status  = clearPreferredDevicesForCapturePreset(capturePreset);
+                if (status == AudioSystem.SUCCESS) {
+                   saveClearPreferredDevicesForCapturePreset(capturePreset);
+                }
+                return status;
+            }
         }
-        return status;
     }
 
     // Only used for external requests coming from an API
     private int clearPreferredDevicesForCapturePreset(int capturePreset) {
-        int status  = AudioSystem.ERROR;
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = clearDevicesRoleForCapturePreset(
+        return clearDevicesRoleForCapturePreset(
                     capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
-        }
-        return status;
     }
 
     // Only used for internal requests
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e83b036..561030e 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1915,7 +1915,7 @@
         // Restore call state
         synchronized (mDeviceBroker.mSetModeLock) {
             onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
-                    mContext.getPackageName(), true /*force*/);
+                    mContext.getPackageName(), true /*force*/, false /*signal*/);
         }
         final int forSys;
         synchronized (mSettingsLock) {
@@ -4743,17 +4743,50 @@
                 }
             }
             if (updateAudioMode) {
-                sendMsg(mAudioHandler,
-                        MSG_UPDATE_AUDIO_MODE,
-                        existingMsgPolicy,
-                        AudioSystem.MODE_CURRENT,
-                        android.os.Process.myPid(),
-                        mContext.getPackageName(),
-                        delay);
+                postUpdateAudioMode(existingMsgPolicy, AudioSystem.MODE_CURRENT,
+                        android.os.Process.myPid(), mContext.getPackageName(),
+                        false /*signal*/, delay);
             }
         }
     }
 
+    static class UpdateAudioModeInfo {
+        UpdateAudioModeInfo(int mode, int pid, String packageName, boolean signal) {
+            mMode = mode;
+            mPid = pid;
+            mPackageName = packageName;
+            mSignal = signal;
+        }
+        private final int mMode;
+        private final int mPid;
+        private final String mPackageName;
+        private final boolean mSignal;
+
+        int getMode() {
+            return mMode;
+        }
+        int getPid() {
+            return mPid;
+        }
+        String getPackageName() {
+            return mPackageName;
+        }
+        boolean getSignal() {
+            return mSignal;
+        }
+    }
+
+    void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName,
+            boolean signal, int delay) {
+        synchronized (mAudioModeResetLock) {
+            if (signal) {
+                mAudioModeResetCount++;
+            }
+            sendMsg(mAudioHandler, MSG_UPDATE_AUDIO_MODE, msgPolicy, 0, 0,
+                new UpdateAudioModeInfo(mode, pid, packageName, signal), delay);
+        }
+    }
+
     private final IRecordingConfigDispatcher mVoiceRecordingActivityMonitor =
             new IRecordingConfigDispatcher.Stub() {
         @Override
@@ -6152,13 +6185,9 @@
                 } else {
                     SetModeDeathHandler h = mSetModeDeathHandlers.get(index);
                     mSetModeDeathHandlers.remove(index);
-                    sendMsg(mAudioHandler,
-                            MSG_UPDATE_AUDIO_MODE,
-                            SENDMSG_QUEUE,
-                            AudioSystem.MODE_CURRENT,
-                            android.os.Process.myPid(),
-                            mContext.getPackageName(),
-                            0);
+                    postUpdateAudioMode(SENDMSG_QUEUE, AudioSystem.MODE_CURRENT,
+                            android.os.Process.myPid(), mContext.getPackageName(),
+                            false /*signal*/, 0);
                 }
             }
         }
@@ -6404,19 +6433,14 @@
                 }
             }
 
-            sendMsg(mAudioHandler,
-                    MSG_UPDATE_AUDIO_MODE,
-                    SENDMSG_REPLACE,
-                    mode,
-                    pid,
-                    callingPackage,
-                    0);
+            postUpdateAudioMode(SENDMSG_REPLACE, mode, pid, callingPackage,
+                    hasModifyPhoneStatePermission && mode == AudioSystem.MODE_NORMAL, 0);
         }
     }
 
     @GuardedBy("mDeviceBroker.mSetModeLock")
     void onUpdateAudioMode(int requestedMode, int requesterPid, String requesterPackage,
-                           boolean force) {
+                           boolean force, boolean signal) {
         if (requestedMode == AudioSystem.MODE_CURRENT) {
             requestedMode = getMode();
         }
@@ -6431,7 +6455,7 @@
         }
         if (DEBUG_MODE) {
             Log.v(TAG, "onUpdateAudioMode() new mode: " + mode + ", current mode: "
-                    + mMode.get() + " requested mode: " + requestedMode);
+                    + mMode.get() + " requested mode: " + requestedMode + " signal: " + signal);
         }
         if (mode != mMode.get() || force) {
             int status = AudioSystem.SUCCESS;
@@ -6477,7 +6501,7 @@
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
-                mDeviceBroker.postSetModeOwner(mode, pid, uid);
+                mDeviceBroker.postSetModeOwner(mode, pid, uid, signal);
             } else {
                 Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode);
             }
@@ -10162,7 +10186,7 @@
                         h.setRecordingActive(isRecordingActiveForUid(h.getUid()));
                         if (wasActive != h.isActive()) {
                             onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
-                                    mContext.getPackageName(), false /*force*/);
+                                    mContext.getPackageName(), false /*force*/, false /*signal*/);
                         }
                     }
                     break;
@@ -10192,7 +10216,9 @@
 
                 case MSG_UPDATE_AUDIO_MODE:
                     synchronized (mDeviceBroker.mSetModeLock) {
-                        onUpdateAudioMode(msg.arg1, msg.arg2, (String) msg.obj, false /*force*/);
+                        UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj;
+                        onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(),
+                                false /*force*/, info.getSignal());
                     }
                     break;
 
@@ -10895,9 +10921,59 @@
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
         mmi.record();
+        //delay abandon focus requests from Telecom if an audio mode reset from Telecom
+        // is still being processed
+        final boolean abandonFromTelecom = (mContext.checkCallingOrSelfPermission(
+                    MODIFY_PHONE_STATE) == PackageManager.PERMISSION_GRANTED)
+                && ((aa != null && aa.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                        || AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId));
+        if (abandonFromTelecom) {
+            synchronized (mAudioModeResetLock) {
+                final long start = java.lang.System.currentTimeMillis();
+                long elapsed = 0;
+                while (mAudioModeResetCount > 0) {
+                    if (DEBUG_MODE) {
+                        Log.i(TAG, "Abandon focus from Telecom, waiting for mode change");
+                    }
+                    try {
+                        mAudioModeResetLock.wait(
+                                AUDIO_MODE_RESET_TIMEOUT_MS - elapsed);
+                    } catch (InterruptedException e) {
+                        Log.w(TAG, "Interrupted while waiting for audio mode reset");
+                    }
+                    elapsed = java.lang.System.currentTimeMillis() - start;
+                    if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) {
+                        Log.e(TAG, "Timeout waiting for audio mode reset");
+                        break;
+                    }
+                }
+                if (DEBUG_MODE && elapsed != 0) {
+                    Log.i(TAG, "Abandon focus from Telecom done waiting");
+                }
+            }
+        }
         return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
     }
 
+    /** synchronization between setMode(NORMAL) and abandonAudioFocus() frmo Telecom */
+    private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000;
+
+    private final Object mAudioModeResetLock = new Object();
+
+    @GuardedBy("mAudioModeResetLock")
+    private int mAudioModeResetCount = 0;
+
+    void decrementAudioModeResetCount() {
+        synchronized (mAudioModeResetLock) {
+            if (mAudioModeResetCount > 0) {
+                mAudioModeResetCount--;
+            } else {
+                Log.w(TAG, "mAudioModeResetCount already 0");
+            }
+            mAudioModeResetLock.notify();
+        }
+    }
+
     /** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */
     public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId,
             AudioAttributes aa, String callingPackageName) {
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index ce92dfb..b421264 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -605,7 +605,11 @@
                 break;
             case BluetoothProfile.LE_AUDIO:
                 if (mLeAudio != null && mLeAudioCallback != null) {
-                    mLeAudio.unregisterCallback(mLeAudioCallback);
+                    try {
+                        mLeAudio.unregisterCallback(mLeAudioCallback);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+                    }
                 }
                 mLeAudio = null;
                 mLeAudioCallback = null;
@@ -682,12 +686,21 @@
                     return;
                 }
                 if (mLeAudio != null && mLeAudioCallback != null) {
-                    mLeAudio.unregisterCallback(mLeAudioCallback);
+                    try {
+                        mLeAudio.unregisterCallback(mLeAudioCallback);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+                    }
                 }
                 mLeAudio = (BluetoothLeAudio) proxy;
                 mLeAudioCallback = new MyLeAudioCallback();
-                mLeAudio.registerCallback(
-                            mContext.getMainExecutor(), mLeAudioCallback);
+                try{
+                    mLeAudio.registerCallback(
+                                mContext.getMainExecutor(), mLeAudioCallback);
+                } catch (Exception e) {
+                    mLeAudioCallback = null;
+                    Log.e(TAG, "Exception while registering callback for LE audio", e);
+                }
                 break;
             case BluetoothProfile.A2DP_SINK:
             case BluetoothProfile.LE_AUDIO_BROADCAST:
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index feef540..4c91789 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -725,7 +725,7 @@
                 return -1;
             }
 
-            if (!Utils.isValidAuthenticatorConfig(promptInfo)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), promptInfo)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -763,7 +763,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1038,7 +1038,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1060,7 +1060,7 @@
 
             Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 407ef1e..de7bce7 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
@@ -233,17 +234,18 @@
      * @param promptInfo
      * @return
      */
-    static boolean isValidAuthenticatorConfig(PromptInfo promptInfo) {
+    static boolean isValidAuthenticatorConfig(Context context, PromptInfo promptInfo) {
         final int authenticators = promptInfo.getAuthenticators();
-        return isValidAuthenticatorConfig(authenticators);
+        return isValidAuthenticatorConfig(context, authenticators);
     }
 
     /**
-     * Checks if the authenticator configuration is a valid combination of the public APIs
-     * @param authenticators
-     * @return
+     * Checks if the authenticator configuration is a valid combination of the public APIs.
+     *
+     * throws {@link SecurityException} if the caller requests for mandatory biometrics without
+     * {@link SET_BIOMETRIC_DIALOG_ADVANCED} permission
      */
-    static boolean isValidAuthenticatorConfig(int authenticators) {
+    static boolean isValidAuthenticatorConfig(Context context, int authenticators) {
         // The caller is not required to set the authenticators. But if they do, check the below.
         if (authenticators == 0) {
             return true;
@@ -271,6 +273,9 @@
         } else if (biometricBits == Authenticators.BIOMETRIC_WEAK) {
             return true;
         } else if (isMandatoryBiometricsRequested(authenticators)) {
+            //TODO(b/347123256): Update CTS test
+            context.enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_ADVANCED,
+                    "Must have SET_BIOMETRIC_DIALOG_ADVANCED permission");
             return true;
         }
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index dc263c5..af9c9ac 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1080,7 +1080,7 @@
      */
     public float[] getNits() {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mNits;
+            return mEvenDimmerBrightnessData.nits;
         }
         return mNits;
     }
@@ -1093,7 +1093,7 @@
     @VisibleForTesting
     public float[] getBacklight() {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mBacklight;
+            return mEvenDimmerBrightnessData.backlight;
         }
         return mBacklight;
     }
@@ -1107,7 +1107,7 @@
      */
     public float getBacklightFromBrightness(float brightness) {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mBrightnessToBacklight.interpolate(brightness);
+            return mEvenDimmerBrightnessData.brightnessToBacklight.interpolate(brightness);
         }
         return mBrightnessToBacklightSpline.interpolate(brightness);
     }
@@ -1120,7 +1120,7 @@
      */
     public float getBrightnessFromBacklight(float backlight) {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mBacklightToBrightness.interpolate(backlight);
+            return mEvenDimmerBrightnessData.backlightToBrightness.interpolate(backlight);
         }
         return mBacklightToBrightnessSpline.interpolate(backlight);
     }
@@ -1131,7 +1131,7 @@
      */
     private Spline getBacklightToBrightnessSpline() {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mBacklightToBrightness;
+            return mEvenDimmerBrightnessData.backlightToBrightness;
         }
         return mBacklightToBrightnessSpline;
     }
@@ -1144,11 +1144,11 @@
      */
     public float getNitsFromBacklight(float backlight) {
         if (mEvenDimmerBrightnessData != null) {
-            if (mEvenDimmerBrightnessData.mBacklightToNits == null) {
+            if (mEvenDimmerBrightnessData.backlightToNits == null) {
                 return INVALID_NITS;
             }
             backlight = Math.max(backlight, mBacklightMinimum);
-            return mEvenDimmerBrightnessData.mBacklightToNits.interpolate(backlight);
+            return mEvenDimmerBrightnessData.backlightToNits.interpolate(backlight);
         }
 
         if (mBacklightToNitsSpline == null) {
@@ -1165,14 +1165,14 @@
      */
     public float getBacklightFromNits(float nits) {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mNitsToBacklight.interpolate(nits);
+            return mEvenDimmerBrightnessData.nitsToBacklight.interpolate(nits);
         }
         return mNitsToBacklightSpline.interpolate(nits);
     }
 
     private Spline getNitsToBacklightSpline() {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mNitsToBacklight;
+            return mEvenDimmerBrightnessData.nitsToBacklight;
         }
         return mNitsToBacklightSpline;
     }
@@ -1186,7 +1186,7 @@
         if (mEvenDimmerBrightnessData == null) {
             return INVALID_NITS;
         }
-        return mEvenDimmerBrightnessData.mMinLuxToNits.interpolate(lux);
+        return mEvenDimmerBrightnessData.minLuxToNits.interpolate(lux);
     }
 
     /**
@@ -1197,7 +1197,7 @@
         if (mEvenDimmerBrightnessData == null) {
             return PowerManager.BRIGHTNESS_MIN;
         }
-        return mEvenDimmerBrightnessData.mTransitionPoint;
+        return mEvenDimmerBrightnessData.transitionPoint;
     }
 
     /**
@@ -1268,7 +1268,7 @@
      */
     public float[] getBrightness() {
         if (mEvenDimmerBrightnessData != null) {
-            return mEvenDimmerBrightnessData.mBrightness;
+            return mEvenDimmerBrightnessData.brightness;
         }
         return mBrightness;
     }
@@ -2617,13 +2617,13 @@
                 List<NonNegativeFloatToFloatPoint> points = map.getMap().getPoint();
                 for (NonNegativeFloatToFloatPoint point : points) {
                     float lux = point.getFirst().floatValue();
-                    float maxBrightness = point.getSecond().floatValue();
-                    if (maxBrightness > hbmTransitionPoint) {
+                    float maxBacklight = point.getSecond().floatValue();
+                    if (maxBacklight > hbmTransitionPoint) {
                         Slog.wtf(TAG,
-                                "Invalid NBM config: maxBrightness is greater than hbm"
+                                "Invalid NBM config: maxBacklight is greater than hbm"
                                         + ".transitionPoint. type="
-                                        + type + "; lux=" + lux + "; maxBrightness="
-                                        + maxBrightness);
+                                        + type + "; lux=" + lux + "; maxBacklight="
+                                        + maxBacklight);
                         continue;
                     }
                     if (luxToTransitionPointMap.containsKey(lux)) {
@@ -2632,8 +2632,7 @@
                                         + lux);
                         continue;
                     }
-                    luxToTransitionPointMap.put(lux,
-                            getBrightnessFromBacklight(maxBrightness));
+                    luxToTransitionPointMap.put(lux, getBrightnessFromBacklight(maxBacklight));
                 }
                 if (!luxToTransitionPointMap.isEmpty()) {
                     mLuxThrottlingData.put(mappedType, luxToTransitionPointMap);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ae33b83..1f9eb08 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3377,10 +3377,18 @@
     private void dumpInternal(PrintWriter pw) {
         pw.println("DISPLAY MANAGER (dumpsys display)");
         BrightnessTracker brightnessTrackerLocal;
+        SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>();
+        int displayPowerControllerCount;
 
         synchronized (mSyncRoot) {
             brightnessTrackerLocal = mBrightnessTracker;
 
+            displayPowerControllerCount = mDisplayPowerControllers.size();
+            for (int i = 0; i < displayPowerControllerCount; i++) {
+                displayPowerControllersLocal.put(
+                        mDisplayPowerControllers.keyAt(i), mDisplayPowerControllers.valueAt(i));
+            }
+
             pw.println("  mSafeMode=" + mSafeMode);
             pw.println("  mPendingTraversal=" + mPendingTraversal);
             pw.println("  mViewports=" + mViewports);
@@ -3451,13 +3459,6 @@
                         + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested);
             }
 
-            final int displayPowerControllerCount = mDisplayPowerControllers.size();
-            pw.println();
-            pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
-            for (int i = 0; i < displayPowerControllerCount; i++) {
-                mDisplayPowerControllers.valueAt(i).dump(pw);
-            }
-
             pw.println();
             mPersistentDataStore.dump(pw);
 
@@ -3470,6 +3471,12 @@
                 mDisplayWindowPolicyControllers.valueAt(i).second.dump("  ", pw);
             }
         }
+        pw.println();
+        pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
+        for (int i = 0; i < displayPowerControllerCount; i++) {
+            displayPowerControllersLocal.valueAt(i).dump(pw);
+        }
+
         if (brightnessTrackerLocal != null) {
             pw.println();
             brightnessTrackerLocal.dump(pw);
@@ -5293,6 +5300,17 @@
         }
 
         @Override
+        public IntArray getDisplayIds() {
+            IntArray displayIds = new IntArray();
+            synchronized (mSyncRoot) {
+                mLogicalDisplayMapper.forEachLocked((logicalDisplay -> {
+                    displayIds.add(logicalDisplay.getDisplayIdLocked());
+                }), /* includeDisabled= */ false);
+            }
+            return displayIds;
+        }
+
+        @Override
         public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader(
                 int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) {
             if (!mFlags.isDisplayOffloadEnabled()) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 711bc79..62d8761 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -22,6 +22,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.brightness.BrightnessEvent.FLAG_EVEN_DIMMER;
 import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
 
 import android.animation.Animator;
@@ -1755,6 +1756,8 @@
         final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f,
                 clampedState.getMinBrightness(), clampedMax,
                 brightnessState);
+        final boolean evenDimmerModeOn =
+                mCdsi != null && mCdsi.getReduceBrightColorsActivatedForEvenDimmer();
         mTempBrightnessEvent.setPercent(Math.round(
                 1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma(
                         brightnessOnAvailableScale) / 10)); // rounded to one dp
@@ -1769,7 +1772,8 @@
         mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
         mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
                 | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
-                | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
+                | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)
+                | (evenDimmerModeOn ? FLAG_EVEN_DIMMER : 0));
         mTempBrightnessEvent.setRbcStrength(mCdsi != null
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
         mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0570b2a..5edea0a 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,9 +37,9 @@
 import android.os.Trace;
 import android.util.DisplayUtils;
 import android.util.LongSparseArray;
-import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.Spline;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.DisplayCutout;
@@ -81,10 +81,6 @@
     private static final String UNIQUE_ID_PREFIX = "local:";
 
     private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
-    // Min and max strengths for even dimmer feature.
-    private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
-    private static final float EVEN_DIMMER_MAX_STRENGTH = 90.0f;
-    private static final float BRIGHTNESS_MIN = 0.0f;
 
     private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
 
@@ -99,7 +95,9 @@
     private Context mOverlayContext;
 
     private int mEvenDimmerStrength = -1;
+    private boolean mEvenDimmerEnabled = false;
     private ColorDisplayService.ColorDisplayServiceInternal mCdsi;
+    private Spline mNitsToEvenDimmerStrength;
 
     // Called with SyncRoot lock held.
     LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
@@ -279,7 +277,7 @@
             mIsFirstDisplay = isFirstDisplay;
             updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs);
             mSidekickInternal = LocalServices.getService(SidekickInternal.class);
-            mBacklightAdapter = new BacklightAdapter(displayToken, isFirstDisplay,
+            mBacklightAdapter = mInjector.getBacklightAdapter(displayToken, isFirstDisplay,
                     mSurfaceControlProxy);
             mActiveSfDisplayModeAtStartId = dynamicInfo.activeDisplayModeId;
         }
@@ -998,26 +996,36 @@
                     }
 
                     private void applyColorMatrixBasedDimming(float brightnessState) {
-                        int strength = (int) (MathUtils.constrainedMap(
-                                // to this range:
-                                EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH,
-                                // from this range:
-                                BRIGHTNESS_MIN, mDisplayDeviceConfig.getEvenDimmerTransitionPoint(),
-                                // map this (+ rounded up):
-                                brightnessState) + 0.5);
-
-                        if (mEvenDimmerStrength < 0 // uninitialised
-                                || MathUtils.abs(mEvenDimmerStrength - strength) > 1
-                                || strength <= 1) {
-                            mEvenDimmerStrength = strength;
-                        }
-                        boolean enabled = mEvenDimmerStrength > 0.0f;
-
                         if (mCdsi == null) {
                             mCdsi = LocalServices.getService(
                                     ColorDisplayService.ColorDisplayServiceInternal.class);
                         }
-                        if (mCdsi != null) {
+                        if (mCdsi == null) {
+                            return;
+                        }
+
+                        final float minHardwareNits = backlightToNits(brightnessToBacklight(
+                                mDisplayDeviceConfig.getEvenDimmerTransitionPoint()));
+                        final float requestedNits =
+                                backlightToNits(brightnessToBacklight(brightnessState));
+                        mNitsToEvenDimmerStrength =
+                                mCdsi.fetchEvenDimmerSpline(minHardwareNits);
+
+                        if (mNitsToEvenDimmerStrength == null) {
+                            return;
+                        }
+
+                        // Find required dimming strength, rounded up.
+                        int strength = Math.round(mNitsToEvenDimmerStrength
+                                .interpolate(requestedNits));
+                        boolean enabled = strength > 0.0f;
+                        if (mEvenDimmerEnabled != enabled) {
+                            Slog.i(TAG, "Setting Extra Dim; strength: " + strength
+                                    + ", " + (enabled ? "enabled" : "disabled"));
+                        }
+                        if (mEvenDimmerStrength != strength || mEvenDimmerEnabled != enabled) {
+                            mEvenDimmerEnabled = enabled;
+                            mEvenDimmerStrength = strength;
                             mCdsi.applyEvenDimmerColorChanges(enabled, strength);
                         }
                     }
@@ -1290,6 +1298,9 @@
             pw.println("DisplayDeviceConfig: ");
             pw.println("---------------------");
             pw.println(mDisplayDeviceConfig);
+            pw.println("mEvenDimmerEnabled=" + mEvenDimmerEnabled);
+            pw.println("mEvenDimmerStrength=" + mEvenDimmerStrength);
+            pw.println("mNitsToEvenDimmerStrength=" + mNitsToEvenDimmerStrength);
         }
 
         private int findSfDisplayModeIdLocked(int displayModeId, int modeGroup) {
@@ -1461,6 +1472,12 @@
                 long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
             return DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay, flags);
         }
+
+        public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+                SurfaceControlProxy surfaceControlProxy) {
+            return new BacklightAdapter(displayToken, isFirstDisplay, surfaceControlProxy);
+
+        }
     }
 
     public interface DisplayEventListener {
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index ad57ebf..9e9b899 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -44,6 +44,7 @@
     public static final int FLAG_DOZE_SCALE = 0x4;
     public static final int FLAG_USER_SET = 0x8;
     public static final int FLAG_LOW_POWER_MODE = 0x20;
+    public static final int FLAG_EVEN_DIMMER = 0x40;
 
     private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
 
@@ -492,6 +493,7 @@
                 + ((mFlags & FLAG_RBC) != 0 ? "rbc " : "")
                 + ((mFlags & FLAG_INVALID_LUX) != 0 ? "invalid_lux " : "")
                 + ((mFlags & FLAG_DOZE_SCALE) != 0 ? "doze_scale " : "")
-                + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "");
+                + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "")
+                + ((mFlags & FLAG_EVEN_DIMMER) != 0 ? "even_dimmer " : "");
     }
 }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index bd759f3..dc59e66 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -70,6 +70,7 @@
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseIntArray;
+import android.util.Spline;
 import android.view.Display;
 import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
@@ -114,6 +115,8 @@
         Matrix.setIdentityM(MATRIX_IDENTITY, 0);
     }
 
+    private static final int EVEN_DIMMER_MAX_PERCENT_ALLOWED = 100;
+
     private static final int MSG_USER_CHANGED = 0;
     private static final int MSG_SET_UP = 1;
     private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -193,6 +196,9 @@
     private final boolean mVisibleBackgroundUsersEnabled;
     private final UserManagerService mUserManager;
 
+    private Spline mEvenDimmerSpline;
+    private boolean mEvenDimmerActivated;
+
     public ColorDisplayService(Context context) {
         super(context);
         mHandler = new TintHandler(DisplayThread.get().getLooper());
@@ -1625,6 +1631,16 @@
         }
 
         /**
+         * Gets the adjusted nits, given a strength and nits.
+         * @param strength of reduce bright colors
+         * @param nits target nits
+         * @return the actual nits that would be output, given input nits and rbc strength.
+         */
+        public float getAdjustedNitsForStrength(float nits, int strength) {
+            return mReduceBrightColorsTintController.getAdjustedNitsForStrength(nits, strength);
+        }
+
+        /**
          * Sets the listener and returns whether reduce bright colors is currently enabled.
          */
         public boolean setReduceBrightColorsListener(ReduceBrightColorsListener listener) {
@@ -1644,6 +1660,14 @@
         }
 
         /**
+         *
+         * @return whether reduce bright colors is on, due to even dimmer being activated
+         */
+        public boolean getReduceBrightColorsActivatedForEvenDimmer() {
+            return mEvenDimmerActivated;
+        }
+
+        /**
          * Gets the computed brightness, in nits, when the reduce bright colors feature is applied
          * at the current strength.
          *
@@ -1667,10 +1691,42 @@
          * Applies tint changes for even dimmer feature.
          */
         public void applyEvenDimmerColorChanges(boolean enabled, int strength) {
+            mEvenDimmerActivated = enabled;
             mReduceBrightColorsTintController.setActivated(enabled);
             mReduceBrightColorsTintController.setMatrix(strength);
             mHandler.sendEmptyMessage(MSG_APPLY_REDUCE_BRIGHT_COLORS);
         }
+
+        /**
+         * Get spline to map between requested nits, and required even dimmer strength.
+         * @return nits to strength spline
+         */
+        public Spline fetchEvenDimmerSpline(float nits) {
+            if (mEvenDimmerSpline == null) {
+                mEvenDimmerSpline = createNitsToStrengthSpline(nits);
+            }
+            return mEvenDimmerSpline;
+        }
+
+        /**
+         * Creates a spline mapping requested nits values, for each resulting strength value
+         * (in percent) for even dimmer.
+         * Returns null if coefficients are not initialised.
+         * @return spline from nits to strength
+         */
+        private Spline createNitsToStrengthSpline(float nits) {
+            final float[] requestedNits = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+            final float[] resultingStrength = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+            for (int i = 0; i <= EVEN_DIMMER_MAX_PERCENT_ALLOWED; i++) {
+                resultingStrength[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] = i;
+                requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] =
+                        getAdjustedNitsForStrength(nits, i);
+                if (requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] == 0) {
+                    return null;
+                }
+            }
+            return new Spline.LinearSpline(requestedNits, resultingStrength);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
index f529c4c..4f6fbd0 100644
--- a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
+++ b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
@@ -123,6 +123,14 @@
         return computeComponentValue(mStrength) * nits;
     }
 
+    /**
+     * Returns the effective brightness (in nits), which has been adjusted to account for the effect
+     * of the bright color reduction.
+     */
+    public float getAdjustedNitsForStrength(float nits, int strength) {
+        return computeComponentValue(strength) * nits;
+    }
+
     private float computeComponentValue(int strengthLevel) {
         final float percentageStrength = strengthLevel / 100f;
         final float squaredPercentageStrength = percentageStrength * percentageStrength;
diff --git a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
index 7e2e10a..9a590a2 100644
--- a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
+++ b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
@@ -34,72 +34,76 @@
     /**
      * Brightness value at which even dimmer methods are used.
      */
-    public final float mTransitionPoint;
+    public final float transitionPoint;
 
     /**
      * Nits array, maps to mBacklight
      */
-    public final float[] mNits;
+    public final float[] nits;
 
     /**
      * Backlight array, maps to mBrightness and mNits
      */
-    public final float[] mBacklight;
+    public final float[] backlight;
 
     /**
      * Brightness array, maps to mBacklight
      */
-    public final float[] mBrightness;
+    public final float[] brightness;
+
     /**
      * Spline, mapping between backlight and nits
      */
-    public final Spline mBacklightToNits;
+    public final Spline backlightToNits;
+
     /**
      * Spline, mapping between nits and backlight
      */
-    public final Spline mNitsToBacklight;
+    public final Spline nitsToBacklight;
+
     /**
      * Spline, mapping between brightness and backlight
      */
-    public final Spline mBrightnessToBacklight;
+    public final Spline brightnessToBacklight;
+
     /**
      * Spline, mapping between backlight and brightness
      */
-    public final Spline mBacklightToBrightness;
+    public final Spline backlightToBrightness;
 
     /**
      * Spline, mapping the minimum nits for each lux condition.
      */
-    public final Spline mMinLuxToNits;
+    public final Spline minLuxToNits;
 
     @VisibleForTesting
     public EvenDimmerBrightnessData(float transitionPoint, float[] nits,
             float[] backlight, float[] brightness, Spline backlightToNits,
             Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness,
             Spline minLuxToNits) {
-        mTransitionPoint = transitionPoint;
-        mNits = nits;
-        mBacklight = backlight;
-        mBrightness = brightness;
-        mBacklightToNits = backlightToNits;
-        mNitsToBacklight = nitsToBacklight;
-        mBrightnessToBacklight = brightnessToBacklight;
-        mBacklightToBrightness = backlightToBrightness;
-        mMinLuxToNits = minLuxToNits;
+        this.transitionPoint = transitionPoint;
+        this.nits = nits;
+        this.backlight = backlight;
+        this.brightness = brightness;
+        this.backlightToNits = backlightToNits;
+        this.nitsToBacklight = nitsToBacklight;
+        this.brightnessToBacklight = brightnessToBacklight;
+        this.backlightToBrightness = backlightToBrightness;
+        this.minLuxToNits = minLuxToNits;
     }
 
     @Override
     public String toString() {
         return "EvenDimmerBrightnessData {"
-                + "mTransitionPoint: " + mTransitionPoint
-                + ", mNits: " + Arrays.toString(mNits)
-                + ", mBacklight: " + Arrays.toString(mBacklight)
-                + ", mBrightness: " + Arrays.toString(mBrightness)
-                + ", mBacklightToNits: " + mBacklightToNits
-                + ", mNitsToBacklight: " + mNitsToBacklight
-                + ", mBrightnessToBacklight: " + mBrightnessToBacklight
-                + ", mBacklightToBrightness: " + mBacklightToBrightness
-                + ", mMinLuxToNits: " + mMinLuxToNits
+                + "transitionPoint: " + transitionPoint
+                + ", nits: " + Arrays.toString(nits)
+                + ", backlight: " + Arrays.toString(backlight)
+                + ", brightness: " + Arrays.toString(brightness)
+                + ", backlightToNits: " + backlightToNits
+                + ", nitsToBacklight: " + nitsToBacklight
+                + ", brightnessToBacklight: " + brightnessToBacklight
+                + ", backlightToBrightness: " + backlightToBrightness
+                + ", minLuxToNits: " + minLuxToNits
                 + "} ";
     }
 
@@ -122,7 +126,6 @@
         if (map == null) {
             return null;
         }
-        String interpolation = map.getInterpolation();
 
         List<BrightnessPoint> brightnessPoints = map.getBrightnessPoint();
         if (brightnessPoints.isEmpty()) {
@@ -169,22 +172,11 @@
             ++i;
         }
 
-        // Explicitly choose linear interpolation.
-        if ("linear".equals(interpolation)) {
-            return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
-                    new Spline.LinearSpline(backlight, nits),
-                    new Spline.LinearSpline(nits, backlight),
-                    new Spline.LinearSpline(brightness, backlight),
-                    new Spline.LinearSpline(backlight, brightness),
-                    new Spline.LinearSpline(minLux, minNits)
-            );
-        }
-
         return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
-                Spline.createSpline(backlight, nits),
-                Spline.createSpline(nits, backlight),
-                Spline.createSpline(brightness, backlight),
-                Spline.createSpline(backlight, brightness),
+                new Spline.LinearSpline(backlight, nits),
+                new Spline.LinearSpline(nits, backlight),
+                new Spline.LinearSpline(brightness, backlight),
+                new Spline.LinearSpline(backlight, brightness),
                 Spline.createSpline(minLux, minNits)
         );
     }
diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig
index 2f817db..3453668 100644
--- a/services/core/java/com/android/server/flags/pinner.aconfig
+++ b/services/core/java/com/android/server/flags/pinner.aconfig
@@ -9,8 +9,11 @@
 }
 
 flag {
-    name: "skip_home_art_pins"
-    namespace: "system_performance"
-    description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory."
-    bug: "340935152"
-}
\ No newline at end of file
+  name: "pin_global_quota"
+  namespace: "system_performance"
+  description: "This flag controls whether pinner will use a global quota or not"
+  bug: "340935152"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 9281267..99f7f12 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -23,6 +23,7 @@
 import android.hardware.display.DisplayViewport;
 import android.hardware.input.KeyGestureEvent;
 import android.os.IBinder;
+import android.util.SparseBooleanArray;
 import android.view.InputChannel;
 import android.view.inputmethod.InputMethodSubtype;
 
@@ -45,9 +46,11 @@
 
     /**
      * Called by the power manager to tell the input manager whether it should start
-     * watching for wake events.
+     * watching for wake events on given displays.
+     *
+     * @param displayInteractivities Map of display ids to their current interactive state.
      */
-    public abstract void setInteractive(boolean interactive);
+    public abstract void setDisplayInteractivities(SparseBooleanArray displayInteractivities);
 
     /**
      * Toggles Caps Lock state for input device with specific id.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index fd7479e..f045576 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,6 +25,7 @@
 
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
 
 import android.Manifest;
 import android.annotation.EnforcePermission;
@@ -95,6 +96,7 @@
 import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
@@ -117,6 +119,7 @@
 import android.view.VerifiedInputEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
+import android.view.WindowManagerPolicyConstants;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
@@ -325,6 +328,9 @@
     // Manages Sticky modifier state
     private final StickyModifierStateController mStickyModifierStateController;
     private final KeyGestureController mKeyGestureController;
+    /** Fallback actions by key code */
+    private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
+            new SparseArray<>();
 
     // Manages Keyboard microphone mute led
     private final KeyboardLedController mKeyboardLedController;
@@ -611,6 +617,7 @@
         mKeyRemapper.systemRunning();
         mPointerIconCache.systemRunning();
         mKeyboardGlyphManager.systemRunning();
+        mKeyGestureController.systemRunning();
 
         initKeyGestures();
     }
@@ -2469,6 +2476,14 @@
                 mFocusEventDebugView.reportKeyEvent(event);
             }
         }
+        if (useKeyGestureEventHandler() && mKeyGestureController.interceptKeyBeforeQueueing(event,
+                policyFlags)) {
+            // If key gesture gets triggered, we send the event to policy with KEY_GESTURE flag
+            // indicating, the event is used in triggering a key gesture. We can't block event
+            // like Power or volume keys since policy might still want to handle it to change
+            // certain states.
+            policyFlags |= WindowManagerPolicyConstants.FLAG_KEY_GESTURE_TRIGGERED;
+        }
         return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
     }
 
@@ -2511,6 +2526,74 @@
                 null, null, null) == PERMISSION_GRANTED;
     }
 
+    // Native callback.
+    @SuppressWarnings("unused")
+    private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
+        if (interceptUnhandledKey(event, focus)) {
+            return null;
+        }
+        // TODO(b/358569822): Move fallback logic to KeyGestureController
+        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
+            return null;
+        }
+        final KeyCharacterMap kcm = event.getKeyCharacterMap();
+        final int keyCode = event.getKeyCode();
+        final int metaState = event.getMetaState();
+        final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
+                && event.getRepeatCount() == 0;
+
+        // Check for fallback actions specified by the key character map.
+        final KeyCharacterMap.FallbackAction fallbackAction;
+        if (initialDown) {
+            fallbackAction = kcm.getFallbackAction(keyCode, metaState);
+        } else {
+            fallbackAction = mFallbackActions.get(keyCode);
+        }
+
+        if (fallbackAction == null) {
+            return null;
+        }
+        KeyEvent fallbackEvent = null;
+        final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
+        fallbackEvent = KeyEvent.obtain(
+                event.getDownTime(), event.getEventTime(),
+                event.getAction(), fallbackAction.keyCode,
+                event.getRepeatCount(), fallbackAction.metaState,
+                event.getDeviceId(), event.getScanCode(),
+                flags, event.getSource(), event.getDisplayId(), null);
+
+        if (!interceptFallback(focus, fallbackEvent, policyFlags)) {
+            fallbackEvent.recycle();
+            fallbackEvent = null;
+        }
+
+        if (initialDown) {
+            mFallbackActions.put(keyCode, fallbackAction);
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            mFallbackActions.remove(keyCode);
+            fallbackAction.recycle();
+        }
+        return fallbackEvent;
+    }
+
+    private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
+            int policyFlags) {
+        int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
+        if ((actions & ACTION_PASS_TO_USER) == 0) {
+            return false;
+        }
+        long delayMillis = interceptKeyBeforeDispatching(focusedToken, fallbackEvent, policyFlags);
+        return delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken);
+    }
+
+    private boolean interceptUnhandledKey(KeyEvent event, IBinder focus) {
+        if (useKeyGestureEventHandler() && mKeyGestureController.interceptUnhandledKey(event,
+                focus)) {
+            return true;
+        }
+        return mWindowManagerCallbacks.interceptUnhandledKey(event, focus);
+    }
+
     private void initKeyGestures() {
         if (!useKeyGestureEventHandler()) {
             return;
@@ -2566,12 +2649,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
-        return mWindowManagerCallbacks.dispatchUnhandledKey(focus, event, policyFlags);
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private void onPointerDownOutsideFocus(IBinder touchedToken) {
         mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken);
     }
@@ -2986,9 +3063,9 @@
         long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags);
 
         /**
-         * Dispatch unhandled key
+         * Intercept unhandled key
          */
-        KeyEvent dispatchUnhandledKey(IBinder token, KeyEvent event, int policyFlags);
+        boolean interceptUnhandledKey(KeyEvent event, IBinder token);
 
         int getPointerLayer();
 
@@ -3261,10 +3338,22 @@
         }
 
         @Override
-        public void setInteractive(boolean interactive) {
-            mNative.setInteractive(interactive);
-            mBatteryController.onInteractiveChanged(interactive);
-            mKeyboardBacklightController.onInteractiveChanged(interactive);
+        public void setDisplayInteractivities(SparseBooleanArray displayInteractivities) {
+            boolean globallyInteractive = false;
+            ArraySet<Integer> nonInteractiveDisplays = new ArraySet<>();
+            for (int i = 0; i < displayInteractivities.size(); i++) {
+                final int displayId = displayInteractivities.keyAt(i);
+                final boolean displayInteractive = displayInteractivities.get(displayId);
+                if (displayInteractive) {
+                    globallyInteractive = true;
+                } else {
+                    nonInteractiveDisplays.add(displayId);
+                }
+            }
+            mNative.setNonInteractiveDisplays(
+                    nonInteractiveDisplays.stream().mapToInt(Integer::intValue).toArray());
+            mBatteryController.onInteractiveChanged(globallyInteractive);
+            mKeyboardBacklightController.onInteractiveChanged(globallyInteractive);
         }
 
         // TODO(b/358569822): Remove this method from InputManagerInternal after key gesture
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 4538b49..b488db5 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -16,14 +16,23 @@
 
 package com.android.server.input;
 
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
+
+import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
 
 import android.annotation.BinderThread;
 import android.annotation.MainThread;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.hardware.input.AidlKeyGestureEvent;
 import android.hardware.input.IKeyGestureEventListener;
 import android.hardware.input.IKeyGestureHandler;
@@ -35,18 +44,23 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.policy.KeyCombinationManager;
 
 import java.util.ArrayDeque;
 import java.util.HashSet;
@@ -82,20 +96,36 @@
     private static final int SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1;
     private static final int LAST_SEARCH_KEY_BEHAVIOR = SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY;
 
+    // must match: config_keyChordPowerVolumeUp in config.xml
+    static final int POWER_VOLUME_UP_BEHAVIOR_NOTHING = 0;
+    static final int POWER_VOLUME_UP_BEHAVIOR_MUTE = 1;
+    static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
+
     private final Context mContext;
     private final Handler mHandler;
     private final int mSystemPid;
+    private final KeyCombinationManager mKeyCombinationManager;
+    private final SettingsObserver mSettingsObserver;
 
     // Pending actions
     private boolean mPendingMetaAction;
     private boolean mPendingCapsLockToggle;
     private boolean mPendingHideRecentSwitcher;
 
-    // Key behaviors
+    // Platform behaviors
     private boolean mEnableBugReportKeyboardShortcut;
+    private boolean mHasFeatureWatch;
+    private boolean mHasFeatureLeanback;
+
+    // Key behaviors
     private int mSearchKeyBehavior;
     private int mSettingsKeyBehavior;
 
+    // Settings behaviors
+    private int mRingerToggleChord = Settings.Secure.VOLUME_HUSH_OFF;
+    private int mPowerVolUpBehavior;
+
+
     // List of currently registered key gesture event listeners keyed by process pid
     @GuardedBy("mKeyGestureEventListenerRecords")
     private final SparseArray<KeyGestureEventListenerRecord>
@@ -128,12 +158,19 @@
                 return Integer.compare(p1, p2);
             }
         });
+        mKeyCombinationManager = new KeyCombinationManager(mHandler);
+        mSettingsObserver = new SettingsObserver(mHandler);
         initBehaviors();
+        initKeyCombinationRules();
     }
 
     private void initBehaviors() {
         mEnableBugReportKeyboardShortcut = "1".equals(SystemProperties.get("ro.debuggable"));
 
+        PackageManager pm = mContext.getPackageManager();
+        mHasFeatureWatch = pm.hasSystemFeature(FEATURE_WATCH);
+        mHasFeatureLeanback = pm.hasSystemFeature(FEATURE_LEANBACK);
+
         Resources res = mContext.getResources();
         mSearchKeyBehavior = res.getInteger(R.integer.config_searchKeyBehavior);
         if (mSearchKeyBehavior < SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH
@@ -145,6 +182,251 @@
                 || mSettingsKeyBehavior > LAST_SETTINGS_KEY_BEHAVIOR) {
             mSettingsKeyBehavior = SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY;
         }
+
+        mHandler.post(this::initBehaviorsFromSettings);
+    }
+
+    private void initBehaviorsFromSettings() {
+        ContentResolver resolver = mContext.getContentResolver();
+        mRingerToggleChord = Settings.Secure.getIntForUser(resolver,
+                Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_OFF,
+                UserHandle.USER_CURRENT);
+
+        mPowerVolUpBehavior = Settings.Global.getInt(resolver,
+                Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
+                mContext.getResources().getInteger(
+                        com.android.internal.R.integer.config_keyChordPowerVolumeUp));
+    }
+
+    private void initKeyCombinationRules() {
+        if (!useKeyGestureEventHandler() || !useKeyGestureEventHandlerMultiPressGestures()) {
+            return;
+        }
+        // TODO(b/358569822): Handle Power, Back key properly since key combination gesture is
+        //  captured here and rest of the Power, Back key behaviors are handled in PWM
+        final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_enableScreenshotChord);
+
+        if (screenshotChordEnabled) {
+            mKeyCombinationManager.addRule(
+                    new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+                            KeyEvent.KEYCODE_POWER) {
+                        @Override
+                        public boolean preCondition() {
+                            return isKeyGestureSupported(
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+                        }
+
+                        @Override
+                        public void execute() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                                    KeyGestureEvent.ACTION_GESTURE_START, 0);
+                        }
+
+                        @Override
+                        public void cancel() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                                    KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                    KeyGestureEvent.FLAG_CANCELLED);
+                        }
+                    });
+
+            if (mHasFeatureWatch) {
+                mKeyCombinationManager.addRule(
+                        new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_POWER,
+                                KeyEvent.KEYCODE_STEM_PRIMARY) {
+                            @Override
+                            public boolean preCondition() {
+                                return isKeyGestureSupported(
+                                        KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+                            }
+
+                            @Override
+                            public void execute() {
+                                handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+                                                KeyEvent.KEYCODE_STEM_PRIMARY},
+                                        KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                                        KeyGestureEvent.ACTION_GESTURE_START, 0);
+                            }
+                            @Override
+                            public void cancel() {
+                                handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+                                                KeyEvent.KEYCODE_STEM_PRIMARY},
+                                        KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                                        KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                        KeyGestureEvent.FLAG_CANCELLED);
+                            }
+                        });
+            }
+        }
+
+        mKeyCombinationManager.addRule(
+                new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+                        KeyEvent.KEYCODE_VOLUME_UP) {
+                    @Override
+                    public boolean preCondition() {
+                        return isKeyGestureSupported(
+                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+                    }
+
+                    @Override
+                    public void execute() {
+                        handleKeyGesture(
+                                new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+                                KeyGestureEvent.ACTION_GESTURE_START, 0);
+                    }
+
+                    @Override
+                    public void cancel() {
+                        handleKeyGesture(
+                                new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                KeyGestureEvent.FLAG_CANCELLED);
+                    }
+                });
+
+        // Volume up + power can either be the "ringer toggle chord" or as another way to
+        // launch GlobalActions. This behavior can change at runtime so we must check behavior
+        // inside the TwoKeysCombinationRule.
+        mKeyCombinationManager.addRule(
+                new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_UP,
+                        KeyEvent.KEYCODE_POWER) {
+                    @Override
+                    public boolean preCondition() {
+                        if (!isKeyGestureSupported(getGestureType())) {
+                            return false;
+                        }
+                        switch (mPowerVolUpBehavior) {
+                            case POWER_VOLUME_UP_BEHAVIOR_MUTE:
+                                return mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF;
+                            default:
+                                return true;
+                        }
+                    }
+                    @Override
+                    public void execute() {
+                        int gestureType = getGestureType();
+                        if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+                            return;
+                        }
+                        handleKeyGesture(
+                                new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+                                gestureType, KeyGestureEvent.ACTION_GESTURE_START, 0);
+                    }
+                    @Override
+                    public void cancel() {
+                        int gestureType = getGestureType();
+                        if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+                            return;
+                        }
+                        handleKeyGesture(
+                                new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+                                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                KeyGestureEvent.FLAG_CANCELLED);
+                    }
+
+                    @KeyGestureEvent.KeyGestureType
+                    private int getGestureType() {
+                        switch (mPowerVolUpBehavior) {
+                            case POWER_VOLUME_UP_BEHAVIOR_MUTE -> {
+                                return KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD;
+                            }
+                            case POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS -> {
+                                return KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS;
+                            }
+                            default -> {
+                                return KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED;
+                            }
+                        }
+                    }
+                });
+
+        if (mHasFeatureLeanback) {
+            mKeyCombinationManager.addRule(
+                    new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+                            KeyEvent.KEYCODE_DPAD_DOWN) {
+                        @Override
+                        public boolean preCondition() {
+                            return isKeyGestureSupported(
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+                        }
+
+                        @Override
+                        public void execute() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                                    KeyGestureEvent.ACTION_GESTURE_START, 0);
+                        }
+
+                        @Override
+                        public void cancel() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                                    KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                    KeyGestureEvent.FLAG_CANCELLED);
+                        }
+                        @Override
+                        public long getKeyInterceptDelayMs() {
+                            // Use a timeout of 0 to prevent additional latency in processing of
+                            // this key. This will potentially cause some unwanted UI actions if the
+                            // user does end up triggering the key combination later, but in most
+                            // cases, the user will simply hit a single key, and this will allow us
+                            // to process it without first waiting to see if the combination is
+                            // going to be triggered.
+                            return 0;
+                        }
+                    });
+
+            mKeyCombinationManager.addRule(
+                    new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+                            KeyEvent.KEYCODE_DPAD_CENTER) {
+                        @Override
+                        public boolean preCondition() {
+                            return isKeyGestureSupported(
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
+                        }
+
+                        @Override
+                        public void execute() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+                                    KeyGestureEvent.ACTION_GESTURE_START, 0);
+                        }
+                        @Override
+                        public void cancel() {
+                            handleKeyGesture(
+                                    new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+                                    KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                    KeyGestureEvent.FLAG_CANCELLED);
+                        }
+                        @Override
+                        public long getKeyInterceptDelayMs() {
+                            return 0;
+                        }
+                    });
+        }
+    }
+
+    public void systemRunning() {
+        mSettingsObserver.observe();
+    }
+
+    public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+        final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
+        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+            return mKeyCombinationManager.interceptKey(event, interactive);
+        }
+        return false;
     }
 
     public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
@@ -153,9 +435,22 @@
         //  KeyGestureHandler (PWM is one of the handlers)
         final int keyCode = event.getKeyCode();
         final int deviceId = event.getDeviceId();
+        final int flags = event.getFlags();
         final long keyConsumed = -1;
         final long keyNotConsumed = 0;
 
+        if (mKeyCombinationManager.isKeyConsumed(event)) {
+            return keyConsumed;
+        }
+
+        if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+            final long now = SystemClock.uptimeMillis();
+            final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
+            if (now < interceptTimeout) {
+                return interceptTimeout - now;
+            }
+        }
+
         Set<Integer> consumedKeys = mConsumedKeysForDevice.get(deviceId);
         if (consumedKeys == null) {
             consumedKeys = new HashSet<>();
@@ -577,10 +872,71 @@
         return false;
     }
 
+    boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+        final int keyCode = event.getKeyCode();
+        final int repeatCount = event.getRepeatCount();
+        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+        final int metaState = event.getModifiers();
+        final int deviceId = event.getDeviceId();
+        final int displayId = event.getDisplayId();
+
+        switch(keyCode) {
+            case KeyEvent.KEYCODE_SPACE:
+                if (down && repeatCount == 0) {
+                    // Handle keyboard layout switching. (CTRL + SPACE)
+                    if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
+                            KeyEvent.META_CTRL_ON)) {
+                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                                KeyEvent.META_CTRL_ON | (event.isShiftPressed()
+                                        ? KeyEvent.META_SHIFT_ON : 0),
+                                KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+                                focusedToken, /* flags = */0);
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_Z:
+                if (down && KeyEvent.metaStateHasModifiers(metaState,
+                        KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) {
+                    // Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
+                    return handleKeyGesture(deviceId, new int[]{keyCode},
+                            KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+                            KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+                            KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+                            focusedToken, /* flags = */0);
+                }
+                break;
+            case KeyEvent.KEYCODE_SYSRQ:
+                if (down && repeatCount == 0) {
+                    return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+                            KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+                            KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+                            focusedToken, /* flags = */0);
+                }
+                break;
+            case KeyEvent.KEYCODE_ESCAPE:
+                if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) {
+                    return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+                            KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+                            KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+                            focusedToken, /* flags = */0);
+                }
+                break;
+        }
+
+        return false;
+    }
+
+    private boolean handleKeyGesture(int[] keycodes,
+            @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
+        return handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
+                gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags);
+    }
+
     @VisibleForTesting
     boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
-            IBinder focusedToken, int flags) {
+            @Nullable IBinder focusedToken, int flags) {
         return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes,
                 modifierState, gestureType, action, displayId, flags), focusedToken);
     }
@@ -628,7 +984,7 @@
     @MainThread
     private void notifyKeyGestureEvent(AidlKeyGestureEvent event) {
         InputDevice device = getInputDevice(event.deviceId);
-        if (device == null || device.isVirtual()) {
+        if (device == null) {
             return;
         }
         if (event.action == KeyGestureEvent.ACTION_GESTURE_COMPLETE) {
@@ -822,6 +1178,27 @@
         }
     }
 
+    private class SettingsObserver extends ContentObserver {
+        private SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        private void observe() {
+            ContentResolver resolver = mContext.getContentResolver();
+            resolver.registerContentObserver(Settings.Secure.getUriFor(
+                            Settings.Secure.VOLUME_HUSH_GESTURE), false, this,
+                    UserHandle.USER_ALL);
+            resolver.registerContentObserver(Settings.Global.getUriFor(
+                            Settings.Global.KEY_CHORD_POWER_VOLUME_UP), false, this,
+                    UserHandle.USER_ALL);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            initBehaviorsFromSettings();
+        }
+    }
+
     @Nullable
     private InputDevice getInputDevice(int deviceId) {
         InputManager inputManager = mContext.getSystemService(InputManager.class);
@@ -851,6 +1228,8 @@
         ipw.println("mPendingHideRecentSwitcher = " + mPendingHideRecentSwitcher);
         ipw.println("mSearchKeyBehavior = " + mSearchKeyBehavior);
         ipw.println("mSettingsKeyBehavior = " + mSettingsKeyBehavior);
+        ipw.println("mRingerToggleChord = " + mRingerToggleChord);
+        ipw.println("mPowerVolUpBehavior = " + mPowerVolUpBehavior);
         ipw.print("mKeyGestureEventListenerRecords = {");
         synchronized (mKeyGestureEventListenerRecords) {
             int size = mKeyGestureEventListenerRecords.size();
@@ -881,5 +1260,6 @@
             ipw.println(ev);
         }
         ipw.decreaseIndent();
+        mKeyCombinationManager.dump("", ipw);
     }
 }
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index d17e256..4404d63 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -141,7 +141,7 @@
 
     void setShowTouches(boolean enabled);
 
-    void setInteractive(boolean interactive);
+    void setNonInteractiveDisplays(int[] displayIds);
 
     void reloadCalibration();
 
@@ -409,7 +409,7 @@
         public native void setShowTouches(boolean enabled);
 
         @Override
-        public native void setInteractive(boolean interactive);
+        public native void setNonInteractiveDisplays(int[] displayIds);
 
         @Override
         public native void reloadCalibration();
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index a1e5ebc..cf0c5b0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -180,7 +180,8 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) {
+        if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+                || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 41313fa..ef1220f 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -33,9 +33,6 @@
     @GuardedBy("ImfLock.class")
     private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
 
-    HardwareKeyboardShortcutController() {
-    }
-
     @GuardedBy("ImfLock.class")
     void update(@NonNull InputMethodSettings settings) {
         mSubtypeHandles.clear();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
index 6cd2493..fc4c0fc 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
@@ -40,6 +40,7 @@
                 if (KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS.equals(name)) {
                     mHideImeWhenNoEditorFocus = properties.getBoolean(name,
                             true /* defaultValue */);
+                    break;
                 }
             }
         };
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2bb2b7b..f0fb33e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4799,7 +4799,8 @@
                 userData.mImeBindingState.mFocusedWindowEditorInfo,
                 info.focusedWindowName, userData.mImeBindingState.mFocusedWindowSoftInputMode,
                 reason, userData.mInFullscreenMode, info.requestWindowName,
-                info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName));
+                info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName,
+                userId));
 
         if (statsToken != null) {
             mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
@@ -6132,8 +6133,7 @@
             dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
         }
 
-        // TODO(b/365868861): Make StartInputHistory, SoftInputShowHideHistory and ImeTracker per
-        //  user.
+        // TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
         synchronized (ImfLock.class) {
             p.println("  mStartInputHistory:");
             mStartInputHistory.dump(pw, "    ");
diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
index 3023603..8445632 100644
--- a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
+++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.os.SystemClock;
 import android.view.WindowManager;
 import android.view.inputmethod.EditorInfo;
@@ -62,6 +63,8 @@
         final String mImeTargetNameFromWm;
         @Nullable
         final String mImeSurfaceParentName;
+        @UserIdInt
+        final int mImeUserId;
 
         Entry(ClientState client, EditorInfo editorInfo,
                 String focusedWindowName,
@@ -69,7 +72,7 @@
                 @SoftInputShowHideReason int reason,
                 boolean inFullscreenMode, String requestWindowName,
                 @Nullable String imeControlTargetName, @Nullable String imeTargetName,
-                @Nullable String imeSurfaceParentName) {
+                @Nullable String imeSurfaceParentName, @UserIdInt int imeUserId) {
             mClientState = client;
             mEditorInfo = editorInfo;
             mFocusedWindowName = focusedWindowName;
@@ -82,6 +85,7 @@
             mImeControlTargetName = imeControlTargetName;
             mImeTargetNameFromWm = imeTargetName;
             mImeSurfaceParentName = imeSurfaceParentName;
+            mImeUserId = imeUserId;
         }
     }
 
@@ -102,7 +106,8 @@
                 continue;
             }
             pw.print(prefix);
-            pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
+            pw.println("SoftInputShowHide[" + entry.mImeUserId + "] #"
+                    + entry.mSequenceNumber + ":");
 
             pw.print(prefix);
             pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 214aa1d..49d4332 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -394,6 +394,7 @@
                                     flags),
                     this::offload).get();
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new RuntimeException(e);
         } catch (ExecutionException e) {
             throw new RuntimeException(e);
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 48d24f2..47f579d 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -195,10 +195,9 @@
                     == PackageManager.PERMISSION_GRANTED) {
                 return true;
             }
-            boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA,
-                    mProjectionGrant.uid,
-                    mProjectionGrant.packageName);
-            if (operationActive) {
+            if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA,
+                    mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null,
+                    "recording lockscreen")) {
                 // Some tools use media projection by granting the OP_PROJECT_MEDIA app
                 // op via a shell command. Those tools can be granted keyguard capture
                 return true;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dc62b27..655f2e4 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2579,6 +2579,7 @@
                 mNotificationChannelLogger,
                 mAppOps,
                 mUserProfiles,
+                mUgmInternal,
                 mShowReviewPermissionsNotification,
                 Clock.systemUTC());
         mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
@@ -4116,23 +4117,38 @@
         }
 
         @Override
-        @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
-        public boolean canBePromoted(String pkg, int uid) {
+        @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+        public boolean appCanBePromoted(String pkg, int uid) {
             checkCallerIsSystemOrSystemUiOrShell();
-            if (!android.app.Flags.uiRichOngoing()) {
+            if (!android.app.Flags.apiRichOngoing()) {
                 return false;
             }
             return mPreferencesHelper.canBePromoted(pkg, uid);
         }
 
         @Override
-        @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
-        public void setCanBePromoted(String pkg, int uid, boolean promote) {
+        @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+        public boolean canBePromoted(String callingPkg) {
+            checkCallerIsSameApp(callingPkg);
+            if (!android.app.Flags.apiRichOngoing()) {
+                return false;
+            }
+            return mPreferencesHelper.canBePromoted(callingPkg, Binder.getCallingUid());
+        }
+
+
+        /**
+         * Any changes from SystemUI or Settings should be fromUser == true. Any changes the
+         * allowlist should be fromUser == false.
+         */
+        @Override
+        @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+        public void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser) {
             checkCallerIsSystemOrSystemUiOrShell();
-            if (!android.app.Flags.uiRichOngoing()) {
+            if (!android.app.Flags.apiRichOngoing()) {
                 return;
             }
-            boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote);
+            boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote, fromUser);
             if (changed) {
                 // check for pending/posted notifs from this app and update the flag
                 synchronized (mNotificationLock) {
@@ -6232,6 +6248,7 @@
             int callingUid = Binder.getCallingUid();
             @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser);
 
+            boolean isSystemCaller = isCallerSystemOrSystemUiOrShell();
             boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
                     && !canManageGlobalZenPolicy(pkg, callingUid);
 
@@ -6268,11 +6285,33 @@
                             policy.priorityCallSenders, policy.priorityMessageSenders,
                             policy.suppressedVisualEffects, currPolicy.priorityConversationSenders);
                 }
+
                 int newVisualEffects = calculateSuppressedVisualEffects(
                             policy, currPolicy, applicationInfo.targetSdkVersion);
-                policy = new Policy(policy.priorityCategories,
-                        policy.priorityCallSenders, policy.priorityMessageSenders,
-                        newVisualEffects, policy.priorityConversationSenders);
+
+                if (android.app.Flags.modesUi()) {
+                    // 1. Callers should not modify STATE_CHANNELS_BYPASSING_DND, which is
+                    // internally calculated and only indicates whether channels that want to bypass
+                    // DND _exist_.
+                    // 2. Only system callers should modify STATE_PRIORITY_CHANNELS_BLOCKED because
+                    // it is @hide.
+                    // 3. If the policy has been modified by the targetSdkVersion checks above then
+                    // it has lost its state flags and that's fine (STATE_PRIORITY_CHANNELS_BLOCKED
+                    // didn't exist until V).
+                    int newState = Policy.STATE_UNSET;
+                    if (isSystemCaller && policy.state != Policy.STATE_UNSET) {
+                        newState = Policy.policyState(
+                                currPolicy.hasPriorityChannels(),
+                                policy.allowPriorityChannels());
+                    }
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, newState, policy.priorityConversationSenders);
+                } else {
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, policy.priorityConversationSenders);
+                }
 
                 if (shouldApplyAsImplicitRule) {
                     mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
@@ -6657,13 +6696,7 @@
             final Uri originalSoundUri =
                     (originalChannel != null) ? originalChannel.getSound() : null;
             if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) {
-                Binder.withCleanCallingIdentity(() -> {
-                    mUgmInternal.checkGrantUriPermission(sourceUid, null,
-                            ContentProvider.getUriWithoutUserId(soundUri),
-                            Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                            ContentProvider.getUserIdFromUri(soundUri,
-                            UserHandle.getUserId(sourceUid)));
-                });
+                PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid);
             }
         }
 
@@ -7776,7 +7809,7 @@
             return false;
         }
 
-        if (android.app.Flags.uiRichOngoing()) {
+        if (android.app.Flags.apiRichOngoing()) {
             // This would normally be done in fixNotification(), but we need the channel info so
             // it's done a little late
             if (mPreferencesHelper.canBePromoted(pkg, notificationUid)
@@ -10740,7 +10773,7 @@
     }
 
     @GuardedBy("mNotificationLock")
-    @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
     private @NonNull List<NotificationRecord> findAppNotificationByListLocked(
             ArrayList<NotificationRecord> list, String pkg, int userId) {
         List<NotificationRecord> records = new ArrayList<>();
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index b9f0968..3ba9384 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -1493,14 +1493,23 @@
 
             final Notification notification = getNotification();
             notification.visitUris((uri) -> {
-                visitGrantableUri(uri, false, false);
+                if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+                    visitGrantableUri(uri, false, false);
+                } else {
+                    oldVisitGrantableUri(uri, false, false);
+                }
             });
 
             if (notification.getChannelId() != null) {
                 NotificationChannel channel = getChannel();
                 if (channel != null) {
-                    visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
-                            & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+                        visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    } else {
+                        oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    }
                 }
             }
         } finally {
@@ -1516,7 +1525,7 @@
      * {@link #mGrantableUris}. Otherwise, this will either log or throw
      * {@link SecurityException} depending on target SDK of enqueuing app.
      */
-    private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
+    private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
         if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
 
         if (mGrantableUris != null && mGrantableUris.contains(uri)) {
@@ -1555,6 +1564,45 @@
         }
     }
 
+    /**
+     * Note the presence of a {@link Uri} that should have permission granted to
+     * whoever will be rendering it.
+     * <p>
+     * If the enqueuing app has the ability to grant access, it will be added to
+     * {@link #mGrantableUris}. Otherwise, this will either log or throw
+     * {@link SecurityException} depending on target SDK of enqueuing app.
+     */
+    private void visitGrantableUri(Uri uri, boolean userOverriddenUri,
+            boolean isSound) {
+        if (mGrantableUris != null && mGrantableUris.contains(uri)) {
+            return; // already verified this URI
+        }
+
+        final int sourceUid = getSbn().getUid();
+        try {
+            PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid);
+
+            if (mGrantableUris == null) {
+                mGrantableUris = new ArraySet<>();
+            }
+            mGrantableUris.add(uri);
+        } catch (SecurityException e) {
+            if (!userOverriddenUri) {
+                if (isSound) {
+                    mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
+                    Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage());
+                } else {
+                    if (mTargetSdkVersion >= Build.VERSION_CODES.P) {
+                        throw e;
+                    } else {
+                        Log.w(TAG,
+                                "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
     public LogMaker getLogMaker(long now) {
         LogMaker lm = getSbn().getLogMaker()
                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance)
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index b6f4889..1464d48 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -25,19 +25,25 @@
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.companion.virtual.VirtualDeviceManager;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.permission.IPermissionManager;
 import android.util.ArrayMap;
 import android.util.Pair;
 import android.util.Slog;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -58,7 +64,7 @@
     private final IPermissionManager mPermManager;
 
     public PermissionHelper(Context context, IPackageManager packageManager,
-            IPermissionManager permManager) {
+                IPermissionManager permManager) {
         mContext = context;
         mPackageManager = packageManager;
         mPermManager = permManager;
@@ -298,6 +304,19 @@
         return false;
     }
 
+    static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri,
+            int sourceUid) {
+        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
+
+        Binder.withCleanCallingIdentity(() -> {
+            // This will throw a SecurityException if the caller can't grant.
+            ugmInternal.checkGrantUriPermission(sourceUid, null,
+                    ContentProvider.getUriWithoutUserId(uri),
+                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
+        });
+    }
+
     public static class PackagePermission {
         public final String packageName;
         public final @UserIdInt int userId;
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index fcc8d2f..9e70f81 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -40,6 +40,7 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -93,6 +94,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -192,10 +194,13 @@
     /**
      * All user-lockable fields for a given application.
      */
-    @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE})
+    @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE,
+            LockableAppFields.USER_LOCKED_BUBBLE,
+            LockableAppFields.USER_LOCKED_PROMOTABLE})
     public @interface LockableAppFields {
         int USER_LOCKED_IMPORTANCE = 0x00000001;
         int USER_LOCKED_BUBBLE = 0x00000002;
+        int USER_LOCKED_PROMOTABLE = 0x00000004;
     }
 
     private final Object mLock = new Object();
@@ -215,6 +220,7 @@
     private final NotificationChannelLogger mNotificationChannelLogger;
     private final AppOpsManager mAppOps;
     private final ManagedServices.UserProfiles mUserProfiles;
+    private final UriGrantsManagerInternal mUgmInternal;
 
     private SparseBooleanArray mBadgingEnabled;
     private SparseBooleanArray mBubblesEnabled;
@@ -235,6 +241,7 @@
             ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
             NotificationChannelLogger notificationChannelLogger,
             AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+            UriGrantsManagerInternal ugmInternal,
             boolean showReviewPermissionsNotification, Clock clock) {
         mContext = context;
         mZenModeHelper = zenHelper;
@@ -245,6 +252,7 @@
         mNotificationChannelLogger = notificationChannelLogger;
         mAppOps = appOpsManager;
         mUserProfiles = userProfiles;
+        mUgmInternal = ugmInternal;
         mShowReviewPermissionsNotification = showReviewPermissionsNotification;
         mIsMediaNotificationFilteringEnabled = context.getResources()
                 .getBoolean(R.bool.config_quickSettingsShowMediaPlayer);
@@ -850,21 +858,27 @@
         }
     }
 
-    @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
     public boolean canBePromoted(String packageName, int uid) {
         synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(packageName, uid).canHavePromotedNotifs;
         }
     }
 
-    @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
-    public boolean setCanBePromoted(String packageName, int uid, boolean promote) {
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+    public boolean setCanBePromoted(String packageName, int uid, boolean promote,
+            boolean fromUser) {
         boolean changed = false;
         synchronized (mLock) {
             PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
-            if (pkgPrefs.canHavePromotedNotifs != promote) {
-                pkgPrefs.canHavePromotedNotifs = promote;
-                changed = true;
+            if (fromUser || ((pkgPrefs.lockedAppFields & USER_LOCKED_PROMOTABLE) == 0)) {
+                if (pkgPrefs.canHavePromotedNotifs != promote) {
+                    pkgPrefs.canHavePromotedNotifs = promote;
+                    if (fromUser) {
+                        pkgPrefs.lockedAppFields |= USER_LOCKED_PROMOTABLE;
+                    }
+                    changed = true;
+                }
             }
         }
         // no need to send a ranking update because we need to update the flag value on all pending
@@ -1159,6 +1173,13 @@
                 }
                 clearLockedFieldsLocked(channel);
 
+                // Verify that the app has permission to read the sound Uri
+                // Only check for new channels, as regular apps can only set sound
+                // before creating. See: {@link NotificationChannel#setSound}
+                if (Flags.notificationVerifyChannelSoundUri()) {
+                    PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
+                }
+
                 channel.setImportanceLockedByCriticalDeviceFunction(
                         r.defaultAppLockedImportance || r.fixedImportance);
 
@@ -3065,7 +3086,7 @@
         boolean migrateToPm = false;
         long creationTime;
 
-        @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+        @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
         boolean canHavePromotedNotifs = false;
 
         @UserIdInt int userId;
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index be3adc1..0b34177 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -144,6 +144,13 @@
 }
 
 flag {
+  name: "notification_minimalism"
+  namespace: "systemui"
+  description: "Minimize the notifications to show on the lockscreen."
+  bug: "330387368"
+}
+
+flag {
   name: "notification_force_group_singletons"
   namespace: "systemui"
   description: "This flag enables forced auto-grouping singleton groups"
@@ -163,3 +170,13 @@
   description: "This flag enables sound uri with vibration source"
   bug: "358524009"
 }
+
+flag {
+  name: "notification_verify_channel_sound_uri"
+  namespace: "systemui"
+  description: "Verify Uri permission for sound when creating a notification channel"
+  bug: "337775777"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index f6d9dc2..03a34f2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -87,6 +87,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
 import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback;
 
 import java.io.FileDescriptor;
@@ -194,9 +195,13 @@
 
             mIsServiceEnabled = isServiceEnabled();
         }
+    }
 
-        //connect to remote services(if available) during boot phase.
-        if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+    @Override
+    public void onUserUnlocked(@NonNull TargetUser user) {
+        Slog.d(TAG, "onUserUnlocked: " + user.getUserHandle());
+        //connect to remote services(if available) during boot.
+        if(user.getUserHandle().equals(UserHandle.SYSTEM)) {
             try {
                 ensureRemoteInferenceServiceInitialized();
                 ensureRemoteIntelligenceServiceInitialized();
diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
index 8410cff..fe9a859 100644
--- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
@@ -171,7 +171,6 @@
     static final int MAX_DATA_BLOCK_SIZE = 1024 * 100;
 
     public static final int DIGEST_SIZE_BYTES = 32;
-    private static final String OEM_UNLOCK_PROP = "sys.oem_unlock_allowed";
     private static final String FLASH_LOCK_PROP = "ro.boot.flash.locked";
     private static final String FLASH_LOCK_LOCKED = "1";
     private static final String FLASH_LOCK_UNLOCKED = "0";
@@ -275,7 +274,6 @@
             enforceChecksumValidity();
             if (mFrpEnforced) {
                 automaticallyDeactivateFrpIfPossible();
-                setOemUnlockEnabledProperty(doGetOemUnlockEnabled());
                 setOldSettingForBackworkCompatibility(mFrpActive);
             } else {
                 formatIfOemUnlockEnabled();
@@ -303,10 +301,6 @@
         }
     }
 
-    private void setOemUnlockEnabledProperty(boolean oemUnlockEnabled) {
-        setProperty(OEM_UNLOCK_PROP, oemUnlockEnabled ? "1" : "0");
-    }
-
     @Override
     public void onBootPhase(int phase) {
         // Wait for initialization in onStart to finish
@@ -342,7 +336,6 @@
                 formatPartitionLocked(true);
             }
         }
-        setOemUnlockEnabledProperty(enabled);
     }
 
     private void enforceOemUnlockReadPermission() {
@@ -814,17 +807,9 @@
             channel.force(true);
         } catch (IOException e) {
             Slog.e(TAG, "unable to access persistent partition", e);
-            return;
-        } finally {
-            setOemUnlockEnabledProperty(enabled);
         }
     }
 
-    @VisibleForTesting
-    void setProperty(String name, String value) {
-        SystemProperties.set(name, value);
-    }
-
     private boolean doGetOemUnlockEnabled() {
         DataInputStream inputStream;
         try {
diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java
new file mode 100644
index 0000000..5f96411
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSource.java
@@ -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.server.pinner;
+
+/* package */ abstract class PinRangeSource {
+    /**
+     * Retrieve a range to pin.
+     *
+     * @param outPinRange Receives the pin region
+     * @return True if we filled in outPinRange or false if we're out of pin entries
+     */
+    abstract boolean read(PinnerService.PinRange outPinRange);
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
new file mode 100644
index 0000000..d6fc487
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
@@ -0,0 +1,37 @@
+/*
+ * 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.server.pinner;
+
+/* package */ class PinRangeSourceStatic extends PinRangeSource {
+    private final int mPinStart;
+    private final int mPinLength;
+    private boolean mDone = false;
+
+    PinRangeSourceStatic(int pinStart, int pinLength) {
+        mPinStart = pinStart;
+        mPinLength = pinLength;
+    }
+
+    @Override
+    boolean read(PinnerService.PinRange outPinRange) {
+        outPinRange.start = mPinStart;
+        outPinRange.length = mPinLength;
+        boolean done = mDone;
+        mDone = true;
+        return !done;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
new file mode 100644
index 0000000..79900b9
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
@@ -0,0 +1,43 @@
+/*
+ * 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.server.pinner;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/* package */ final class PinRangeSourceStream extends PinRangeSource {
+    private final DataInputStream mStream;
+    private boolean mDone = false;
+
+    PinRangeSourceStream(InputStream stream) {
+        mStream = new DataInputStream(stream);
+    }
+
+    @Override
+    boolean read(PinnerService.PinRange outPinRange) {
+        if (!mDone) {
+            try {
+                outPinRange.start = mStream.readInt();
+                outPinRange.length = mStream.readInt();
+            } catch (IOException ex) {
+                mDone = true;
+            }
+        }
+        return !mDone;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java
new file mode 100644
index 0000000..a8de344
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnedFile.java
@@ -0,0 +1,61 @@
+/*
+ * 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.server.pinner;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+@VisibleForTesting
+public final class PinnedFile implements AutoCloseable {
+    private long mAddress;
+    final long mapSize;
+    final String fileName;
+    public final long bytesPinned;
+
+    // Whether this file was pinned using a pinlist
+    boolean used_pinlist;
+
+    // User defined group name for pinner accounting
+    String groupName = "";
+    ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
+
+    public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) {
+        mAddress = address;
+        this.mapSize = mapSize;
+        this.fileName = fileName;
+        this.bytesPinned = bytesPinned;
+    }
+
+    @Override
+    public void close() {
+        if (mAddress >= 0) {
+            PinnerUtils.safeMunmap(mAddress, mapSize);
+            mAddress = -1;
+        }
+        for (PinnedFile dep : pinnedDeps) {
+            if (dep != null) {
+                dep.close();
+            }
+        }
+    }
+
+    @Override
+    public void finalize() {
+        close();
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java
similarity index 76%
rename from services/core/java/com/android/server/PinnerService.java
rename to services/core/java/com/android/server/pinner/PinnerService.java
index ef03888..d7ac520 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/pinner/PinnerService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.server.pinner;
 
 import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
 import static android.app.ActivityManager.UID_OBSERVER_GONE;
 import static android.os.Process.SYSTEM_UID;
 
+import static com.android.server.flags.Flags.pinGlobalQuota;
 import static com.android.server.flags.Flags.pinWebview;
-import static com.android.server.flags.Flags.skipHomeArtPins;
 
 import android.annotation.EnforcePermission;
 import android.annotation.IntDef;
@@ -49,6 +49,7 @@
 import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -72,13 +73,13 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import dalvik.system.DexFile;
 import dalvik.system.VMRuntime;
 
-import java.io.Closeable;
-import java.io.DataInputStream;
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -110,8 +111,7 @@
     private static final String PIN_META_FILENAME = "pinlist.meta";
     private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
     private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY
-            | PackageManager.MATCH_DIRECT_BOOT_AWARE
-            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+            | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 
     private static final int KEY_CAMERA = 0;
     private static final int KEY_HOME = 1;
@@ -126,6 +126,8 @@
 
     public static final String ANON_REGION_STAT_NAME = "[anon]";
 
+    private static final String SYSTEM_GROUP_NAME = "system";
+
     @IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface AppKey {}
@@ -139,7 +141,8 @@
     private final UserManager mUserManager;
 
     /** The list of the statically pinned files. */
-    @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
+    @GuardedBy("this")
+    private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
 
     /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */
     @GuardedBy("this")
@@ -159,8 +162,8 @@
     /**
      * A set of {@link AppKey} that are configured to be pinned.
      */
-    @GuardedBy("this")
-    private ArraySet<Integer> mPinKeys;
+    @GuardedBy("this") private
+    ArraySet<Integer> mPinKeys;
 
     // Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want
     // them to be responsive to dynamic flag changes for experimentation.
@@ -180,14 +183,23 @@
     private final boolean mConfiguredToPinAssistant;
     private final int mConfiguredWebviewPinBytes;
 
+    // This is the percentage of total device memory that will be used to set the global quota.
+    private final int mConfiguredMaxPinnedMemoryPercentage;
+
+    // This is the global pinner quota that can be pinned.
+    private long mConfiguredMaxPinnedMemory;
+
+    // This is the currently pinned memory.
+    private long mCurrentPinnedMemory = 0;
+
     private BinderService mBinderService;
     private PinnerHandler mPinnerHandler = null;
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-          // If an app has updated, update pinned files accordingly.
-          if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
+            // If an app has updated, update pinned files accordingly.
+            if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
                 Uri packageUri = intent.getData();
                 String packageName = packageUri.getSchemeSpecificPart();
                 ArraySet<String> updatedPackages = new ArraySet<>();
@@ -210,7 +222,7 @@
 
     /** Utility class for testing. */
     @VisibleForTesting
-    static class Injector {
+    public static class Injector {
         protected DeviceConfigInterface getDeviceConfigInterface() {
             return DeviceConfigInterface.REAL;
         }
@@ -219,9 +231,9 @@
             service.publishBinderService("pinner", binderService);
         }
 
-        protected PinnedFile pinFileInternal(String fileToPin,
-                int maxBytesToPin, boolean attemptPinIntrospection) {
-            return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
+        protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+                long maxBytesToPin, boolean attemptPinIntrospection) {
+            return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
         }
     }
 
@@ -230,7 +242,7 @@
     }
 
     @VisibleForTesting
-    PinnerService(Context context, Injector injector) {
+    public PinnerService(Context context, Injector injector) {
         super(context);
 
         mContext = context;
@@ -244,6 +256,9 @@
                 com.android.internal.R.bool.config_pinnerAssistantApp);
         mConfiguredWebviewPinBytes = context.getResources().getInteger(
                 com.android.internal.R.integer.config_pinnerWebviewPinBytes);
+        mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage);
+
         mPinKeys = createPinKeys();
         mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
 
@@ -261,10 +276,8 @@
         registerUidListener();
         registerUserSetupCompleteListener();
 
-        mDeviceConfigInterface.addOnPropertiesChangedListener(
-                DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
-                new HandlerExecutor(mPinnerHandler),
-                mDeviceConfigAnonSizeListener);
+        mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
+                new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener);
     }
 
     @Override
@@ -272,6 +285,10 @@
         if (DEBUG) {
             Slog.i(TAG, "Starting PinnerService");
         }
+        mConfiguredMaxPinnedMemory =
+                (Process.getTotalMemory()
+                        * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100))
+                / 100;
         mBinderService = new BinderService();
         mInjector.publishBinderService(this, mBinderService);
         publishLocalService(PinnerService.class, this);
@@ -348,7 +365,7 @@
         protected PinnedFileStats(int uid, PinnedFile file) {
             this.uid = uid;
             this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1);
-            this.sizeKb = file.bytesPinned / 1024;
+            this.sizeKb = (int) file.bytesPinned / 1024;
         }
     }
 
@@ -358,20 +375,11 @@
     private void handlePinOnStart() {
         // Files to pin come from the overlay and can be specified per-device config
         String[] filesToPin = mContext.getResources().getStringArray(
-            com.android.internal.R.array.config_defaultPinnerServiceFiles);
+                com.android.internal.R.array.config_defaultPinnerServiceFiles);
         // Continue trying to pin each file even if we fail to pin some of them
         for (String fileToPin : filesToPin) {
-            PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE,
-                    /*attemptPinIntrospection=*/false);
-            if (pf == null) {
-                Slog.e(TAG, "Failed to pin file = " + fileToPin);
-                continue;
-            }
-            synchronized (this) {
-                mPinnedFiles.put(pf.fileName, pf);
-            }
-            pf.groupName = "system";
-            pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null);
+            pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME,
+                    true);
         }
 
         refreshPinAnonConfig();
@@ -383,10 +391,9 @@
      * regular home app.
      */
     private void registerUserSetupCompleteListener() {
-        Uri userSetupCompleteUri = Settings.Secure.getUriFor(
-                Settings.Secure.USER_SETUP_COMPLETE);
-        mContext.getContentResolver().registerContentObserver(userSetupCompleteUri,
-                false, new ContentObserver(null) {
+        Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+        mContext.getContentResolver().registerContentObserver(
+                userSetupCompleteUri, false, new ContentObserver(null) {
                     @Override
                     public void onChange(boolean selfChange, Uri uri) {
                         if (userSetupCompleteUri.equals(uri)) {
@@ -409,7 +416,7 @@
                 }
 
                 @Override
-                public void onUidActive(int uid)  {
+                public void onUidActive(int uid) {
                     mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
                             PinnerService::handleUidActive, PinnerService.this, uid));
                 }
@@ -423,7 +430,6 @@
         updateActiveState(uid, false /* active */);
         int key;
         synchronized (this) {
-
             // In case we have a pending repin, repin now. See mPendingRepin for more information.
             key = mPendingRepin.getOrDefault(uid, -1);
             if (key == -1) {
@@ -491,8 +497,8 @@
 
     private ApplicationInfo getCameraInfo(int userHandle) {
         Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
-        ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle,
-            false /* defaultToSystemApp */);
+        ApplicationInfo info = getApplicationInfoForIntent(
+                cameraIntent, userHandle, false /* defaultToSystemApp */);
 
         // If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent.
         // We don't use _SECURE first because it will never get set on a device
@@ -501,16 +507,16 @@
         // preference using this intent.
         if (info == null) {
             cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE);
-            info = getApplicationInfoForIntent(cameraIntent, userHandle,
-                false /* defaultToSystemApp */);
+            info = getApplicationInfoForIntent(
+                    cameraIntent, userHandle, false /* defaultToSystemApp */);
         }
 
         // If the _SECURE intent doesn't resolve, try the original intent but request
         // the system app for camera if there was more than one result.
         if (info == null) {
             cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
-            info = getApplicationInfoForIntent(cameraIntent, userHandle,
-                true /* defaultToSystemApp */);
+            info = getApplicationInfoForIntent(
+                    cameraIntent, userHandle, true /* defaultToSystemApp */);
         }
         return info;
     }
@@ -525,14 +531,14 @@
         return getApplicationInfoForIntent(intent, userHandle, true);
     }
 
-    private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle,
-            boolean defaultToSystemApp) {
+    private ApplicationInfo getApplicationInfoForIntent(
+            Intent intent, int userHandle, boolean defaultToSystemApp) {
         if (intent == null) {
             return null;
         }
 
-        ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent,
-                MATCH_FLAGS, userHandle);
+        ResolveInfo resolveInfo =
+                mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle);
 
         // If this intent can resolve to only one app, choose that one.
         // Otherwise, if we've requested to default to the system app, return it;
@@ -547,12 +553,11 @@
         }
 
         if (defaultToSystemApp) {
-            List<ResolveInfo> infoList = mContext.getPackageManager()
-                .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle);
+            List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser(
+                    intent, MATCH_FLAGS, userHandle);
             ApplicationInfo systemAppInfo = null;
             for (ResolveInfo info : infoList) {
-                if ((info.activityInfo.applicationInfo.flags
-                      & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                     if (systemAppInfo == null) {
                         systemAppInfo = info.activityInfo.applicationInfo;
                     } else {
@@ -568,13 +573,13 @@
     }
 
     private void sendPinAppsMessage(int userHandle) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this,
-                userHandle));
+        mPinnerHandler.sendMessage(
+                PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle));
     }
 
     private void sendPinAppsWithUpdatedKeysMessage(int userHandle) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys,
-                this, userHandle));
+        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
+                PinnerService::pinAppsWithUpdatedKeys, this, userHandle));
     }
     private void sendUnpinAppsMessage() {
         mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this));
@@ -586,8 +591,7 @@
         // phenotype property is not set.
         boolean shouldPinCamera = mConfiguredToPinCamera
                 && mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
-                        "pin_camera",
-                        SystemProperties.getBoolean("pinner.pin_camera", true));
+                        "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true));
         if (shouldPinCamera) {
             pinKeys.add(KEY_CAMERA);
         } else if (DEBUG) {
@@ -626,8 +630,9 @@
             synchronized (this) {
                 // This code path demands preceding unpinApps() call.
                 if (!mPinnedApps.isEmpty()) {
-                    Slog.e(TAG, "Attempted to update a list of apps, "
-                            + "but apps were already pinned. Skipping.");
+                    Slog.e(TAG,
+                            "Attempted to update a list of apps, "
+                                    + "but apps were already pinned. Skipping.");
                     return;
                 }
 
@@ -646,8 +651,8 @@
      * @see #pinApp(int, int, boolean)
      */
     private void sendPinAppMessage(int key, int userHandle, boolean force) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this,
-                key, userHandle, force));
+        mPinnerHandler.sendMessage(
+                PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force));
     }
 
     /**
@@ -667,10 +672,10 @@
             }
             return;
         }
-        unpinApp(key);
         ApplicationInfo info = getInfoForKey(key, userHandle);
+        unpinApp(key);
         if (info != null) {
-            pinApp(key, info);
+            pinAppInternal(key, info);
         }
     }
 
@@ -682,9 +687,7 @@
     private int getUidForKey(@AppKey int key) {
         synchronized (this) {
             PinnedApp existing = mPinnedApps.get(key);
-            return existing != null && existing.active
-                    ? existing.uid
-                    : -1;
+            return existing != null && existing.active ? existing.uid : -1;
         }
     }
 
@@ -727,11 +730,8 @@
      * Handle any changes in the anon region pinner config.
      */
     private void refreshPinAnonConfig() {
-        long newPinAnonSize =
-                mDeviceConfigInterface.getLong(
-                        DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
-                        DEVICE_CONFIG_KEY_ANON_SIZE,
-                        DEFAULT_ANON_SIZE);
+        long newPinAnonSize = mDeviceConfigInterface.getLong(
+                DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE);
         newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE));
         if (newPinAnonSize != mPinAnonSize) {
             mPinAnonSize = newPinAnonSize;
@@ -765,10 +765,9 @@
         try {
             // Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status).
             // The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo.
-            address = Os.mmap(0, alignedPinSize,
-                    OsConstants.PROT_READ | OsConstants.PROT_WRITE,
-                    OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS,
-                    new FileDescriptor(), /*offset=*/0);
+            address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE,
+                    OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(),
+                    /*offset=*/0);
 
             Unsafe tempUnsafe = null;
             Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class;
@@ -794,14 +793,14 @@
             return;
         } finally {
             if (address >= 0) {
-                safeMunmap(address, alignedPinSize);
+                PinnerUtils.safeMunmap(address, alignedPinSize);
             }
         }
     }
 
     private void unpinAnonRegion() {
         if (mPinAnonAddress != 0) {
-            safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
+            PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
         }
         mPinAnonAddress = 0;
         mCurrentlyPinnedAnonSize = 0;
@@ -824,12 +823,20 @@
     }
 
     /**
+     * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer
+     * pin any file.
+     */
+    private long getAvailableGlobalQuota() {
+        return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory;
+    }
+
+    /**
      * Pins an application.
      *
      * @param key The key of the app to pin.
      * @param appInfo The corresponding app info.
      */
-    private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) {
+    private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) {
         if (appInfo == null) {
             return;
         }
@@ -839,7 +846,6 @@
             mPinnedApps.put(key, pinnedApp);
         }
 
-
         // pin APK
         final int pinSizeLimit = getSizeLimitForKey(key);
         List<String> apks = new ArrayList<>();
@@ -851,36 +857,31 @@
             }
         }
 
-        int apkPinSizeLimit = pinSizeLimit;
+        long apkPinSizeLimit = pinSizeLimit;
 
-        boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins();
-
-        for (String apk: apks) {
+        for (String apk : apks) {
             if (apkPinSizeLimit <= 0) {
                 Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk);
                 // Continue instead of break to print all skipped APK names.
                 continue;
             }
 
-            PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true);
+            String pinGroup = getNameForKey(key);
+            boolean shouldPinDeps = apk.equals(appInfo.sourceDir);
+            PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps);
             if (pf == null) {
                 Slog.e(TAG, "Failed to pin " + apk);
                 continue;
             }
-            pf.groupName = getNameForKey(key);
 
             if (DEBUG) {
                 Slog.i(TAG, "Pinned " + pf.fileName);
             }
             synchronized (this) {
                 pinnedApp.mFiles.add(pf);
-                mPinnedFiles.put(pf.fileName, pf);
             }
 
             apkPinSizeLimit -= pf.bytesPinned;
-            if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) {
-                pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo);
-            }
         }
     }
 
@@ -892,19 +893,23 @@
      * that related to the file but not within itself.
      *
      * @param fileToPin File to pin
-     * @param maxBytesToPin maximum quota allowed for pinning
-     * @return total bytes that were pinned.
+     * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}.
+     * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned.
+     *                         Note: {@code bytesRequestedToPin} limit will not apply to optimized
+     *                         dependencies pinned, only global quotas will apply instead.
+     * @return pinned file
      */
-    public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo,
-            @Nullable String groupName) {
+    public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin,
+            @Nullable ApplicationInfo appInfo, @Nullable String groupName,
+            boolean pinOptimizedDeps) {
         PinnedFile existingPin;
-        synchronized(this) {
+        synchronized (this) {
             existingPin = mPinnedFiles.get(fileToPin);
         }
         if (existingPin != null) {
-            if (existingPin.bytesPinned == maxBytesToPin) {
+            if (existingPin.bytesPinned == bytesRequestedToPin) {
                 // Duplicate pin requesting same amount of bytes, lets just bail out.
-                return 0;
+                return null;
             } else {
                 // User decided to pin a different amount of bytes than currently pinned
                 // so this is a valid pin request. Unpin the previous version before repining.
@@ -915,26 +920,38 @@
             }
         }
 
+        long remainingQuota = getAvailableGlobalQuota();
+
+        if (pinGlobalQuota()) {
+            if (remainingQuota <= 0) {
+                Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin);
+                return null;
+            }
+            bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota);
+        }
+
         boolean isApk = fileToPin.endsWith(".apk");
-        int bytesPinned = 0;
-        PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin,
+
+        PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin,
                 /*attemptPinIntrospection=*/isApk);
         if (pf == null) {
             Slog.e(TAG, "Failed to pin file = " + fileToPin);
-            return 0;
+            return null;
         }
         pf.groupName = groupName != null ? groupName : "";
 
-        bytesPinned += pf.bytesPinned;
-        maxBytesToPin -= bytesPinned;
+        mCurrentPinnedMemory += pf.bytesPinned;
 
         synchronized (this) {
             mPinnedFiles.put(pf.fileName, pf);
         }
-        if (maxBytesToPin > 0) {
-            pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo);
+
+        if (pinOptimizedDeps) {
+            mCurrentPinnedMemory +=
+                    pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo);
         }
-        return bytesPinned;
+
+        return pf;
     }
 
     /**
@@ -945,13 +962,13 @@
      *                to null it will use the default supported ABI by the device.
      * @return total bytes pinned.
      */
-    private int pinOptimizedDexDependencies(
-            PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) {
+    private long pinOptimizedDexDependencies(
+            PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) {
         if (pinnedFile == null) {
             return 0;
         }
 
-        int bytesPinned = 0;
+        long bytesPinned = 0;
         if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) {
             String abi = null;
             if (appInfo != null) {
@@ -974,7 +991,7 @@
                 // Unpin if it was already pinned prior to re-pinning.
                 unpinFile(file);
 
-                PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin,
+                PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin,
                         /*attemptPinIntrospection=*/false);
                 if (df == null) {
                     Slog.i(TAG, "Failed to pin ART file = " + file);
@@ -992,7 +1009,8 @@
         return bytesPinned;
     }
 
-    /** mlock length bytes of fileToPin in memory
+    /**
+     * mlock length bytes of fileToPin in memory
      *
      * If attemptPinIntrospection is true, then treat the file to pin as a zip file and
      * look for a "pinlist.meta" file in the archive root directory. The structure of this
@@ -1029,8 +1047,8 @@
      *   zip in order to extract the
      * @return Pinned memory resource owner thing or null on error
      */
-    private static PinnedFile pinFileInternal(
-            String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) {
+    private PinnedFile pinFileInternal(
+            String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) {
         if (DEBUG) {
             Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection);
         }
@@ -1054,8 +1072,8 @@
             }
             return pinnedFile;
         } finally {
-            safeClose(pinRangeStream);
-            safeClose(fileAsZip);  // Also closes any streams we've opened
+            PinnerUtils.safeClose(pinRangeStream);
+            PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened
         }
     }
 
@@ -1068,11 +1086,8 @@
         try {
             zip = new ZipFile(fileName);
         } catch (IOException ex) {
-            Slog.w(TAG,
-                   String.format(
-                       "could not open \"%s\" as zip: pinning as blob",
-                                 fileName),
-                   ex);
+            Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName),
+                    ex);
         }
         return zip;
     }
@@ -1112,9 +1127,9 @@
                 pinMetaStream = zipFile.getInputStream(pinMetaEntry);
             } catch (IOException ex) {
                 Slog.w(TAG,
-                       String.format("error reading pin metadata \"%s\": pinning as blob",
-                                     fileName),
-                       ex);
+                        String.format(
+                                "error reading pin metadata \"%s\": pinning as blob", fileName),
+                        ex);
             }
         } else {
             Slog.w(TAG,
@@ -1124,57 +1139,6 @@
         return pinMetaStream;
     }
 
-    private static abstract class PinRangeSource {
-        /** Retrive a range to pin.
-         *
-         * @param outPinRange Receives the pin region
-         * @return True if we filled in outPinRange or false if we're out of pin entries
-         */
-        abstract boolean read(PinRange outPinRange);
-    }
-
-    private static final class PinRangeSourceStatic extends PinRangeSource {
-        private final int mPinStart;
-        private final int mPinLength;
-        private boolean mDone = false;
-
-        PinRangeSourceStatic(int pinStart, int pinLength) {
-            mPinStart = pinStart;
-            mPinLength = pinLength;
-        }
-
-        @Override
-        boolean read(PinRange outPinRange) {
-            outPinRange.start = mPinStart;
-            outPinRange.length = mPinLength;
-            boolean done = mDone;
-            mDone = true;
-            return !done;
-        }
-    }
-
-    private static final class PinRangeSourceStream extends PinRangeSource {
-        private final DataInputStream mStream;
-        private boolean mDone = false;
-
-        PinRangeSourceStream(InputStream stream) {
-            mStream = new DataInputStream(stream);
-        }
-
-        @Override
-        boolean read(PinRange outPinRange) {
-            if (!mDone) {
-                try {
-                    outPinRange.start = mStream.readInt();
-                    outPinRange.length = mStream.readInt();
-                } catch (IOException ex) {
-                    mDone = true;
-                }
-            }
-            return !mDone;
-        }
-    }
-
     /**
      * Helper for pinFile.
      *
@@ -1185,25 +1149,20 @@
      * @return PinnedFile or null on error
      */
     private static PinnedFile pinFileRanges(
-        String fileToPin,
-        int maxBytesToPin,
-        PinRangeSource pinRangeSource)
-    {
+            String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) {
         FileDescriptor fd = new FileDescriptor();
         long address = -1;
-        int mapSize = 0;
+        long mapSize = 0;
 
         try {
             int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC);
             fd = Os.open(fileToPin, openFlags, 0);
             mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
-            address = Os.mmap(0, mapSize,
-                              OsConstants.PROT_READ,
-                              OsConstants.MAP_SHARED,
-                              fd, /*offset=*/0);
+            address = Os.mmap(
+                    0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0);
 
             PinRange pinRange = new PinRange();
-            int bytesPinned = 0;
+            long bytesPinned = 0;
 
             // We pin at page granularity, so make sure the limit is page-aligned
             if (maxBytesToPin % PAGE_SIZE != 0) {
@@ -1211,10 +1170,10 @@
             }
 
             while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
-                int pinStart = pinRange.start;
-                int pinLength = pinRange.length;
-                pinStart = clamp(0, pinStart, mapSize);
-                pinLength = clamp(0, pinLength, mapSize - pinStart);
+                long pinStart = pinRange.start;
+                long pinLength = pinRange.length;
+                pinStart = PinnerUtils.clamp(0, pinStart, mapSize);
+                pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart);
                 pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
 
                 // mlock doesn't require the region to be page-aligned, but we snap the
@@ -1229,14 +1188,13 @@
                 if (pinLength % PAGE_SIZE != 0) {
                     pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
                 }
-                pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
+                pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned);
 
                 if (pinLength > 0) {
                     if (DEBUG) {
                         Slog.d(TAG,
-                               String.format(
-                                   "pinning at %s %s bytes of %s",
-                                   pinStart, pinLength, fileToPin));
+                                String.format("pinning at %s %s bytes of %s", pinStart, pinLength,
+                                        fileToPin));
                     }
                     Os.mlock(address + pinStart, pinLength);
                 }
@@ -1244,15 +1202,15 @@
             }
 
             PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
-            address = -1;  // Ownership transferred
+            address = -1; // Ownership transferred
             return pinnedFile;
         } catch (ErrnoException ex) {
             Slog.e(TAG, "Could not pin file " + fileToPin, ex);
             return null;
         } finally {
-            safeClose(fd);
+            PinnerUtils.safeClose(fd);
             if (address >= 0) {
-                safeMunmap(address, mapSize);
+                PinnerUtils.safeMunmap(address, mapSize);
             }
         }
     }
@@ -1273,81 +1231,50 @@
         }
     }
 
-    public void unpinFile(String filename) {
+    /**
+     * Unpin a file and its optimized dependencies.
+     *
+     * @param filename file to unpin.
+     * @return number of bytes unpinned, 0 in case of failure or nothing to unpin.
+     */
+    public long unpinFile(String filename) {
         PinnedFile pinnedFile;
         synchronized (this) {
             pinnedFile = mPinnedFiles.get(filename);
         }
         if (pinnedFile == null) {
             // File not pinned, nothing to do.
-            return;
+            return 0;
         }
+        long unpinnedBytes = pinnedFile.bytesPinned;
         pinnedFile.close();
         synchronized (this) {
             if (DEBUG) {
                 Slog.d(TAG, "Unpinned file: " + filename);
             }
+            mCurrentPinnedMemory -= pinnedFile.bytesPinned;
+
             mPinnedFiles.remove(pinnedFile.fileName);
             for (PinnedFile dep : pinnedFile.pinnedDeps) {
                 if (dep == null) {
                     continue;
                 }
+                unpinnedBytes -= dep.bytesPinned;
+                mCurrentPinnedMemory -= dep.bytesPinned;
                 mPinnedFiles.remove(dep.fileName);
                 if (DEBUG) {
                     Slog.d(TAG, "Unpinned dependency: " + dep.fileName);
                 }
             }
         }
-    }
 
-    private static int clamp(int min, int value, int max) {
-        return Math.max(min, Math.min(value, max));
-    }
-
-    private static void safeMunmap(long address, long mapSize) {
-        try {
-            Os.munmap(address, mapSize);
-        } catch (ErrnoException ex) {
-            Slog.w(TAG, "ignoring error in unmap", ex);
-        }
-    }
-
-    /**
-     * Close FD, swallowing irrelevant errors.
-     */
-    private static void safeClose(@Nullable FileDescriptor fd) {
-        if (fd != null && fd.valid()) {
-            try {
-                Os.close(fd);
-            } catch (ErrnoException ex) {
-                // Swallow the exception: non-EBADF errors in close(2)
-                // indicate deferred paging write errors, which we
-                // don't care about here. The underlying file
-                // descriptor is always closed.
-                if (ex.errno == OsConstants.EBADF) {
-                    throw new AssertionError(ex);
-                }
-            }
-        }
-    }
-
-    /**
-     * Close closeable thing, swallowing errors.
-     */
-    private static void safeClose(@Nullable Closeable thing) {
-        if (thing != null) {
-            try {
-                thing.close();
-            } catch (IOException ex) {
-                Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
-            }
-        }
+        return unpinnedBytes;
     }
 
     public List<PinnedFileStat> getPinnerStats() {
         ArrayList<PinnedFileStat> stats = new ArrayList<>();
         Collection<PinnedFile> pinnedFiles;
-        synchronized(this) {
+        synchronized (this) {
             pinnedFiles = mPinnedFiles.values();
         }
         for (PinnedFile pf : pinnedFiles) {
@@ -1355,8 +1282,8 @@
             stats.add(stat);
         }
         if (mCurrentlyPinnedAnonSize > 0) {
-            stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME,
-                        mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
+            stats.add(new PinnedFileStat(
+                    ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
         }
         return stats;
     }
@@ -1364,71 +1291,124 @@
     public final class BinderService extends IPinnerService.Stub {
         @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw))
+                return;
             HashSet<PinnedFile> shownPins = new HashSet<>();
-            HashSet<String> groups = new HashSet<>();
-            final int bytesPerMB = 1024 * 1024;
+            HashSet<String> shownGroups = new HashSet<>();
+            HashSet<String> groupsToPrint = new HashSet<>();
+            final double bytesPerMB = 1024 * 1024;
+            pw.format("Pinner Configs:\n");
+            pw.format("   Total Pinner quota: %d%% of total device memory\n",
+                    mConfiguredMaxPinnedMemoryPercentage);
+            pw.format("   Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory,
+                    mConfiguredMaxPinnedMemory / bytesPerMB);
+            pw.format("   Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes);
+            pw.format("\nPinned Files:\n");
             synchronized (PinnerService.this) {
                 long totalSize = 0;
+
+                // We print apps separately from regular pins as they contain extra information that
+                // other pins do not.
                 for (int key : mPinnedApps.keySet()) {
                     PinnedApp app = mPinnedApps.get(key);
                     pw.print(getNameForKey(key));
-                    pw.print(" uid="); pw.print(app.uid);
-                    pw.print(" active="); pw.print(app.active);
+                    pw.print(" uid=");
+                    pw.print(app.uid);
+                    pw.print(" active=");
+                    pw.print(app.active);
+
+                    if (!app.mFiles.isEmpty()) {
+                        shownGroups.add(app.mFiles.getFirst().groupName);
+                    }
                     pw.println();
+                    long bytesPinnedForApp = 0;
+                    long bytesPinnedForAppDeps = 0;
                     for (PinnedFile pf : mPinnedApps.get(key).mFiles) {
                         pw.print("  ");
-                        pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName,
+                        pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName,
                                 pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist);
                         totalSize += pf.bytesPinned;
+                        bytesPinnedForApp += pf.bytesPinned;
                         shownPins.add(pf);
                         for (PinnedFile dep : pf.pinnedDeps) {
                             pw.print("  ");
-                            pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName,
-                                    dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist);
+                            pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+                                    dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+                                    dep.used_pinlist);
                             totalSize += dep.bytesPinned;
+                            bytesPinnedForAppDeps += dep.bytesPinned;
                             shownPins.add(dep);
                         }
                     }
+                    long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps;
+                    pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), "
+                                    + "Dependencies=%d (%.2f MB)]\n\n",
+                            bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB,
+                            bytesPinnedForApp, bytesPinnedForApp / bytesPerMB,
+                            bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB);
                 }
                 pw.println();
                 for (PinnedFile pinnedFile : mPinnedFiles.values()) {
-                    if (!groups.contains(pinnedFile.groupName)) {
-                        groups.add(pinnedFile.groupName);
+                    if (!groupsToPrint.contains(pinnedFile.groupName)
+                            && !shownGroups.contains(pinnedFile.groupName)) {
+                        groupsToPrint.add(pinnedFile.groupName);
                     }
                 }
-                boolean firstPinInGroup = true;
-                for (String group : groups) {
+
+                // Print all the non app groups.
+                for (String group : groupsToPrint) {
                     List<PinnedFile> groupPins = getAllPinsForGroup(group);
+                    pw.print("\nGroup:" + group);
+                    long bytesPinnedForGroupNoDeps = 0;
+                    long bytesPinnedForGroupDeps = 0;
+                    pw.println();
                     for (PinnedFile pinnedFile : groupPins) {
                         if (shownPins.contains(pinnedFile)) {
-                            // Already showed in the dump and accounted for, skip.
+                            // Already displayed and accounted for, skip.
                             continue;
                         }
-                        if (firstPinInGroup) {
-                            firstPinInGroup = false;
-                            // Ensure we only print when there are pins for groups not yet shown
-                            // in the pinned app section.
-                            pw.print("Group:" + group);
-                            pw.println();
-                        }
-                        pw.format("  %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName,
-                                pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB,
-                                pinnedFile.used_pinlist);
+                        pw.format("  %s pinned: %d bytes (%.2f MB) pinlist:%b\n",
+                                pinnedFile.fileName, pinnedFile.bytesPinned,
+                                pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist);
                         totalSize += pinnedFile.bytesPinned;
+                        bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned;
+                        shownPins.add(pinnedFile);
+                        for (PinnedFile dep : pinnedFile.pinnedDeps) {
+                            if (shownPins.contains(dep)) {
+                                // Already displayed and accounted for, skip.
+                                continue;
+                            }
+                            pw.print("  ");
+                            pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+                                    dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+                                    dep.used_pinlist);
+                            totalSize += dep.bytesPinned;
+                            bytesPinnedForGroupDeps += dep.bytesPinned;
+                            shownPins.add(dep);
+                        }
                     }
+                    long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps;
+                    pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), "
+                                    + "Dependencies=%d (%.2f MB)]\n\n",
+                            bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB,
+                            bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB,
+                            bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB);
                 }
                 pw.println();
                 if (mPinAnonAddress != 0) {
-                    pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB);
+                    pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize,
+                            mCurrentlyPinnedAnonSize / bytesPerMB);
                     totalSize += mCurrentlyPinnedAnonSize;
                 }
-                pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB);
+                pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB);
+                pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(),
+                        getAvailableGlobalQuota() / bytesPerMB);
                 pw.println();
                 if (!mPendingRepin.isEmpty()) {
                     pw.print("Pending repin: ");
                     for (int key : mPendingRepin.values()) {
-                        pw.print(getNameForKey(key)); pw.print(' ');
+                        pw.print(getNameForKey(key));
+                        pw.print(' ');
                     }
                     pw.println();
                 }
@@ -1462,8 +1442,9 @@
                     repin();
                     break;
                 default:
-                    printError(out, String.format(
-                            "Unknown pinner command: %s. Supported commands: repin", command));
+                    printError(out,
+                            String.format("Unknown pinner command: %s. Supported commands: repin",
+                                    command));
                     resultReceiver.send(-1, null);
                     return;
             }
@@ -1479,46 +1460,6 @@
         }
     }
 
-    @VisibleForTesting
-    public static final class PinnedFile implements AutoCloseable {
-        private long mAddress;
-        final int mapSize;
-        final String fileName;
-        final int bytesPinned;
-
-        // Whether this file was pinned using a pinlist
-        boolean used_pinlist;
-
-        // User defined group name for pinner accounting
-        String groupName = "";
-        ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
-
-        PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
-             mAddress = address;
-             this.mapSize = mapSize;
-             this.fileName = fileName;
-             this.bytesPinned = bytesPinned;
-        }
-
-        @Override
-        public void close() {
-            if (mAddress >= 0) {
-                safeMunmap(mAddress, mapSize);
-                mAddress = -1;
-            }
-            for (PinnedFile dep : pinnedDeps) {
-                if (dep != null) {
-                    dep.close();
-                }
-            }
-        }
-
-        @Override
-        public void finalize() {
-            close();
-        }
-    }
-
     final static class PinRange {
         int start;
         int length;
@@ -1528,7 +1469,6 @@
      * Represents an app that was pinned.
      */
     private final class PinnedApp {
-
         /**
          * The uid of the package being pinned. This stays constant while the package stays
          * installed.
@@ -1557,11 +1497,9 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case PIN_ONSTART_MSG:
-                {
+                case PIN_ONSTART_MSG: {
                     handlePinOnStart();
-                }
-                break;
+                } break;
 
                 default:
                     super.handleMessage(msg);
diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java
new file mode 100644
index 0000000..a836a83
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnerUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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.server.pinner;
+
+import android.annotation.Nullable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/* package */ final class PinnerUtils {
+    private static final String TAG = "PinnerUtils";
+
+    public static long clamp(long min, long value, long max) {
+        return Math.max(min, Math.min(value, max));
+    }
+
+    public static void safeMunmap(long address, long mapSize) {
+        try {
+            Os.munmap(address, mapSize);
+        } catch (ErrnoException ex) {
+            Slog.w(TAG, "ignoring error in unmap", ex);
+        }
+    }
+
+    /**
+     * Close FD, swallowing irrelevant errors.
+     */
+    public static void safeClose(@Nullable FileDescriptor fd) {
+        if (fd != null && fd.valid()) {
+            try {
+                Os.close(fd);
+            } catch (ErrnoException ex) {
+                // Swallow the exception: non-EBADF errors in close(2)
+                // indicate deferred paging write errors, which we
+                // don't care about here. The underlying file
+                // descriptor is always closed.
+                if (ex.errno == OsConstants.EBADF) {
+                    throw new AssertionError(ex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Close closeable thing, swallowing errors.
+     */
+    public static void safeClose(@Nullable Closeable thing) {
+        if (thing != null) {
+            try {
+                thing.close();
+            } catch (IOException ex) {
+                Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72..89ced12 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2208,10 +2208,10 @@
             return true;
         }
         boolean permissionGranted = requireFullPermission ? hasPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
                 : (hasPermission(
-                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
-                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
         if (!permissionGranted) {
             if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
                 return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 9ecc7b9..1569fa0 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -70,13 +70,13 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
 import com.android.server.art.ArtManagerLocal;
 import com.android.server.art.DexUseManagerLocal;
 import com.android.server.art.ReasonMapping;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DexoptParams;
 import com.android.server.art.model.DexoptResult;
+import com.android.server.pinner.PinnerService;
 import com.android.server.pm.PackageDexOptimizer.DexOptResult;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index f449126..aca65bf 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1019,7 +1019,9 @@
                     && scanInstallPackages(requests, createdAppId, versionInfos)) {
                 List<ReconciledPackage> reconciledPackages =
                         reconcileInstallPackages(requests, versionInfos);
-                if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) {
+                if (reconciledPackages != null
+                        && renameAndUpdatePaths(requests)
+                        && commitInstallPackages(reconciledPackages)) {
                     success = true;
                 }
             }
@@ -1029,24 +1031,49 @@
         }
     }
 
-    private boolean prepareInstallPackages(List<InstallRequest> requests) {
-        // TODO: will remove the locking after doRename is moved out of prepare
+    private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
         try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
             for (InstallRequest request : requests) {
-                try {
-                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
-                    request.onPrepareStarted();
-                    preparePackageLI(request);
-                } catch (PrepareFailure prepareFailure) {
-                    request.setError(prepareFailure.error,
-                            prepareFailure.getMessage());
-                    request.setOriginPackage(prepareFailure.mConflictingPackage);
-                    request.setOriginPermission(prepareFailure.mConflictingPermission);
-                    return false;
-                } finally {
-                    request.onPrepareFinished();
-                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+                ParsedPackage parsedPackage = request.getParsedPackage();
+                final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
+                if (isApex) {
+                    continue;
                 }
+                try {
+                    doRenameLI(request, parsedPackage);
+                    setUpFsVerity(parsedPackage);
+                } catch (Installer.InstallerException | IOException | DigestException
+                         | NoSuchAlgorithmException | PrepareFailure e) {
+                    request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
+                            "Failed to set up verity: " + e);
+                    return false;
+                }
+
+                // update paths that are set before renaming
+                PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
+                scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
+                scannedPackageSetting.setLegacyNativeLibraryPath(
+                        parsedPackage.getNativeLibraryRootDir());
+            }
+            return true;
+        }
+    }
+
+    private boolean prepareInstallPackages(List<InstallRequest> requests) {
+        for (InstallRequest request : requests) {
+            try {
+                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
+                request.onPrepareStarted();
+                preparePackage(request);
+            } catch (PrepareFailure prepareFailure) {
+                request.setError(prepareFailure.error,
+                        prepareFailure.getMessage());
+                request.setOriginPackage(prepareFailure.mConflictingPackage);
+                request.setOriginPermission(prepareFailure.mConflictingPermission);
+                return false;
+            } finally {
+                request.onPrepareFinished();
+                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
             }
         }
         return true;
@@ -1231,8 +1258,7 @@
         return newProp != null && newProp.getBoolean();
     }
 
-    @GuardedBy("mPm.mInstallLock")
-    private void preparePackageLI(InstallRequest request) throws PrepareFailure {
+    private void preparePackage(InstallRequest request) throws PrepareFailure {
         final int[] allUsers =  mPm.mUserManager.getUserIds();
         final int installFlags = request.getInstallFlags();
         final boolean onExternal = request.getVolumeUuid() != null;
@@ -1739,18 +1765,7 @@
             }
         }
 
-        if (!isApex) {
-            doRenameLI(request, parsedPackage);
-
-            try {
-                setUpFsVerity(parsedPackage);
-            } catch (Installer.InstallerException | IOException | DigestException
-                    | NoSuchAlgorithmException e) {
-                throw PrepareFailure.ofInternalError(
-                        "Failed to set up verity: " + e,
-                        PackageManagerException.INTERNAL_ERROR_VERITY_SETUP);
-            }
-        } else {
+        if (isApex) {
             // Use the path returned by apexd
             parsedPackage.setPath(request.getApexInfo().modulePath);
             parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
@@ -2092,7 +2107,21 @@
 
         // Reflect the rename in scanned details
         try {
-            parsedPackage.setPath(afterCodeFile.getCanonicalPath());
+            String afterCanonicalPath = afterCodeFile.getCanonicalPath();
+            String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
+            parsedPackage.setPath(afterCanonicalPath);
+
+            parsedPackage.setNativeLibraryDir(
+                    parsedPackage.getNativeLibraryDir()
+                            .replace(beforeCanonicalPath, afterCanonicalPath));
+            parsedPackage.setNativeLibraryRootDir(
+                    parsedPackage.getNativeLibraryRootDir()
+                            .replace(beforeCanonicalPath, afterCanonicalPath));
+            String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
+            if (secondaryNativeLibraryDir != null) {
+                parsedPackage.setSecondaryNativeLibraryDir(
+                        secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
+            }
         } catch (IOException e) {
             Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);
             throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
@@ -2102,6 +2131,7 @@
                 afterCodeFile, parsedPackage.getBaseApkPath()));
         parsedPackage.setSplitCodePaths(FileUtils.rewriteAfterRename(beforeCodeFile,
                 afterCodeFile, parsedPackage.getSplitCodePaths()));
+        request.updateAllCodePaths(AndroidPackageUtils.getAllCodePaths(parsedPackage));
     }
 
     // TODO(b/168126411): Once staged install flow starts using the same folder as non-staged
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index ae7749b..1f79ac0 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -615,6 +615,20 @@
         return mScanResult.mDynamicSharedLibraryInfos;
     }
 
+    public void updateAllCodePaths(List<String> paths) {
+        if (mScanResult.mSdkSharedLibraryInfo != null) {
+            mScanResult.mSdkSharedLibraryInfo.setAllCodePaths(paths);
+        }
+        if (mScanResult.mStaticSharedLibraryInfo != null) {
+            mScanResult.mStaticSharedLibraryInfo.setAllCodePaths(paths);
+        }
+        if (mScanResult.mDynamicSharedLibraryInfos != null) {
+            for (SharedLibraryInfo info : mScanResult.mDynamicSharedLibraryInfos) {
+                info.setAllCodePaths(paths);
+            }
+        }
+    }
+
     @Nullable
     public PackageSetting getScannedPackageSetting() {
         assertScanResultExists();
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 5653da0..8657de2 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -716,7 +716,7 @@
                     visiblePackages.add(info.getActivityInfo().packageName);
                 }
                 final List<ApplicationInfo> installedPackages =
-                        mPackageManagerInternal.getInstalledApplications(
+                        mPackageManagerInternal.getInstalledApplicationsCrossUser(
                                 /* flags= */ 0, user.getIdentifier(), callingUid);
                 for (ApplicationInfo applicationInfo : installedPackages) {
                     if (!visiblePackages.contains(applicationInfo.packageName)) {
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 1316df1..b1b1637 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -50,6 +50,7 @@
 import android.app.admin.DevicePolicyEventLogger;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
+import android.app.role.RoleManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
@@ -201,6 +202,9 @@
             Manifest.permission.USE_FULL_SCREEN_INTENT
     );
 
+    private static final String ROLE_SYSTEM_APP_PROTECTION_SERVICE =
+            "android.app.role.SYSTEM_APP_PROTECTION_SERVICE";
+
     final PackageArchiver mPackageArchiver;
 
     private final Context mContext;
@@ -1454,6 +1458,12 @@
                     .createEvent(DevicePolicyEnums.UNINSTALL_PACKAGE)
                     .setAdmin(callerPackageName)
                     .write();
+        } else if (isSystemAppProtectionRoleHolder(snapshot, userId, callingUid)) {
+            // Allow the SYSTEM_APP_PROTECTION_SERVICE role holder to silently uninstall, with a
+            // clean calling identity to get DELETE_PACKAGES permission
+            Binder.withCleanCallingIdentity(() ->
+                    mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags)
+            );
         } else {
             ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId);
             if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
@@ -1475,6 +1485,29 @@
         }
     }
 
+    private Boolean isSystemAppProtectionRoleHolder(
+            @NonNull Computer snapshot, int userId, int callingUid) {
+        if (!Flags.deletePackagesSilentlyBackport()) {
+            return false;
+        }
+        String holderPackageName = Binder.withCleanCallingIdentity(() -> {
+            RoleManager roleManager = mPm.mContext.getSystemService(RoleManager.class);
+            if (roleManager == null) {
+                return null;
+            }
+            List<String> holders = roleManager.getRoleHoldersAsUser(
+                    ROLE_SYSTEM_APP_PROTECTION_SERVICE, UserHandle.of(userId));
+            if (holders.isEmpty()) {
+                return null;
+            }
+            return holders.get(0);
+        });
+        if (holderPackageName == null) {
+            return false;
+        }
+        return snapshot.getPackageUid(holderPackageName, /* flags= */ 0, userId) == callingUid;
+    }
+
     @Override
     public void uninstallExistingPackage(VersionedPackage versionedPackage,
             String callerPackageName, IntentSender statusReceiver, int userId) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ff9c3e5..611e0d8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3389,18 +3389,31 @@
                     return true;
                 }
                 // Does it contain a device admin for any user?
-                int[] users;
+                int[] allUsers = mUserManager.getUserIds();
+                int[] targetUsers;
                 if (userId == UserHandle.USER_ALL) {
-                    users = mUserManager.getUserIds();
+                    targetUsers = allUsers;
                 } else {
-                    users = new int[]{userId};
+                    targetUsers = new int[]{userId};
                 }
-                for (int i = 0; i < users.length; ++i) {
-                    if (dpm.packageHasActiveAdmins(packageName, users[i])) {
+
+                for (int i = 0; i < targetUsers.length; ++i) {
+                    if (dpm.packageHasActiveAdmins(packageName, targetUsers[i])) {
                         return true;
                     }
-                    if (isDeviceManagementRoleHolder(packageName, users[i])
-                            && dpmi.isUserOrganizationManaged(users[i])) {
+                }
+
+                // If a package is DMRH on a managed user, it should also be treated as an admin on
+                // that user. If that package is also a system package, it should also be protected
+                // on other users otherwise "uninstall updates" on an unmanaged user may break
+                // management on other users because apk version is shared between all users.
+                var packageState = snapshotComputer().getPackageStateInternal(packageName);
+                if (packageState == null) {
+                    return false;
+                }
+                for (int user : packageState.isSystem() ? allUsers : targetUsers) {
+                    if (isDeviceManagementRoleHolder(packageName, user)
+                            && dpmi.isUserOrganizationManaged(user)) {
                         return true;
                     }
                 }
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index df9f7fb..5fc3e33 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1015,8 +1015,7 @@
                     permission, attributionSource, message, forDataDelivery, startDataDelivery,
                     fromDatasource, attributedOp);
             // Finish any started op if some step in the attribution chain failed.
-            if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
-                    && result != PermissionChecker.PERMISSION_SOFT_DENIED) {
+            if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) {
                 if (attributedOp == AppOpsManager.OP_NONE) {
                     finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
                             attributionSource.asState(), fromDatasource);
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
index 9dfaca8..1592ef3 100644
--- a/services/core/java/com/android/server/policy/KeyCombinationManager.java
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -65,21 +65,21 @@
      *       };
      *  </pre>
      */
-    abstract static class TwoKeysCombinationRule {
+    public abstract static class TwoKeysCombinationRule {
         private int mKeyCode1;
         private int mKeyCode2;
 
-        TwoKeysCombinationRule(int keyCode1, int keyCode2) {
+        public TwoKeysCombinationRule(int keyCode1, int keyCode2) {
             mKeyCode1 = keyCode1;
             mKeyCode2 = keyCode2;
         }
 
-        boolean preCondition() {
+        public boolean preCondition() {
             return true;
         }
 
         boolean shouldInterceptKey(int keyCode) {
-            return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2);
+            return (keyCode == mKeyCode1 || keyCode == mKeyCode2) && preCondition();
         }
 
         boolean shouldInterceptKeys(SparseLongArray downTimes) {
@@ -94,12 +94,12 @@
         }
 
         // The excessive delay before it dispatching to client.
-        long getKeyInterceptDelayMs() {
+        public long getKeyInterceptDelayMs() {
             return COMBINE_KEY_DELAY_MILLIS;
         }
 
-        abstract void execute();
-        abstract void cancel();
+        public abstract void execute();
+        public abstract void cancel();
 
         @Override
         public String toString() {
@@ -128,18 +128,18 @@
         }
     }
 
-    KeyCombinationManager(Handler handler) {
+    public KeyCombinationManager(Handler handler) {
         mHandler = handler;
     }
 
-    void addRule(TwoKeysCombinationRule rule) {
+    public void addRule(TwoKeysCombinationRule rule) {
         if (mRules.contains(rule)) {
             throw new IllegalArgumentException("Rule : " + rule + " already exists.");
         }
         mRules.add(rule);
     }
 
-    void removeRule(TwoKeysCombinationRule rule) {
+    public void removeRule(TwoKeysCombinationRule rule) {
         mRules.remove(rule);
     }
 
@@ -148,7 +148,7 @@
      * to a window.
      * Return true if any active rule could be triggered by the key event, otherwise false.
      */
-    boolean interceptKey(KeyEvent event, boolean interactive) {
+    public boolean interceptKey(KeyEvent event, boolean interactive) {
         synchronized (mLock) {
             return interceptKeyLocked(event, interactive);
         }
@@ -226,7 +226,7 @@
     /**
      * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window.
      */
-    long getKeyInterceptTimeout(int keyCode) {
+    public long getKeyInterceptTimeout(int keyCode) {
         synchronized (mLock) {
             if (mDownTimes.get(keyCode) == 0) {
                 return 0;
@@ -246,7 +246,7 @@
     /**
      * True if the key event had been handled.
      */
-    boolean isKeyConsumed(KeyEvent event) {
+    public boolean isKeyConsumed(KeyEvent event) {
         synchronized (mLock) {
             if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
                 return false;
@@ -258,7 +258,7 @@
     /**
      * True if power key is the candidate.
      */
-    boolean isPowerKeyIntercepted() {
+    public boolean isPowerKeyIntercepted() {
         synchronized (mLock) {
             if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) {
                 // return false if only if power key pressed.
@@ -294,7 +294,7 @@
         return false;
     }
 
-    void dump(String prefix, PrintWriter pw) {
+    public void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "KeyCombination rules:");
         forAllRules(mRules, (rule)-> {
             pw.println(prefix + "  " + rule);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index ca60518..2284050 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -86,6 +86,7 @@
 import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
 import static com.android.hardware.input.Flags.modifierShortcutDump;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
@@ -198,7 +199,6 @@
 import android.view.IDisplayFoldListener;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
-import android.view.KeyCharacterMap.FallbackAction;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
 import android.view.MotionEvent;
@@ -698,10 +698,6 @@
     // Maps global key codes to the components that will handle them.
     private GlobalKeyManager mGlobalKeyManager;
 
-    // Fallback actions by key code.
-    private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
-            new SparseArray<KeyCharacterMap.FallbackAction>();
-
     private final com.android.internal.policy.LogDecelerateInterpolator mLogDecelerateInterpolator
             = new LogDecelerateInterpolator(100, 0);
     private final DeferredKeyActionExecutor mDeferredKeyActionExecutor =
@@ -1056,7 +1052,8 @@
         return handled;
     }
 
-    private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
+    private void interceptPowerKeyDown(KeyEvent event, boolean interactive,
+            boolean isKeyGestureTriggered) {
         // Hold a wake lock until the power key is released.
         if (!mPowerKeyWakeLock.isHeld()) {
             mPowerKeyWakeLock.acquire();
@@ -1089,7 +1086,8 @@
         // If the power key has still not yet been handled, then detect short
         // press, long press, or multi press and decide what to do.
         mPowerKeyHandled = mPowerKeyHandled || hungUp
-                || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
+                || handledByPowerManager || isKeyGestureTriggered
+                || mKeyCombinationManager.isPowerKeyIntercepted();
         if (!mPowerKeyHandled) {
             if (!interactive) {
                 wakeUpFromWakeKey(event);
@@ -2465,6 +2463,9 @@
 
     private void initKeyCombinationRules() {
         mKeyCombinationManager = new KeyCombinationManager(mHandler);
+        if (useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures()) {
+            return;
+        }
         final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_enableScreenshotChord);
 
@@ -2472,13 +2473,13 @@
             mKeyCombinationManager.addRule(
                     new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
                         @Override
-                        void execute() {
+                        public void execute() {
                             mPowerKeyHandled = true;
                             interceptScreenshotChord(
                                     SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
                         }
                         @Override
-                        void cancel() {
+                        public void cancel() {
                             cancelPendingScreenshotChordAction();
                         }
                     });
@@ -2487,13 +2488,13 @@
                 mKeyCombinationManager.addRule(
                         new TwoKeysCombinationRule(KEYCODE_POWER, KEYCODE_STEM_PRIMARY) {
                             @Override
-                            void execute() {
+                            public void execute() {
                                 mPowerKeyHandled = true;
                                 interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
                                         getScreenshotChordLongPressDelay());
                             }
                             @Override
-                            void cancel() {
+                            public void cancel() {
                                 cancelPendingScreenshotChordAction();
                             }
                         });
@@ -2503,16 +2504,16 @@
         mKeyCombinationManager.addRule(
                 new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP) {
                     @Override
-                    boolean preCondition() {
+                    public boolean preCondition() {
                         return mAccessibilityShortcutController
                                 .isAccessibilityShortcutAvailable(isKeyguardLocked());
                     }
                     @Override
-                    void execute() {
+                    public void execute() {
                         interceptAccessibilityShortcutChord();
                     }
                     @Override
-                    void cancel() {
+                    public void cancel() {
                         cancelPendingAccessibilityShortcutAction();
                     }
                 });
@@ -2523,7 +2524,7 @@
         mKeyCombinationManager.addRule(
                 new TwoKeysCombinationRule(KEYCODE_VOLUME_UP, KEYCODE_POWER) {
                     @Override
-                    boolean preCondition() {
+                    public boolean preCondition() {
                         switch (mPowerVolUpBehavior) {
                             case POWER_VOLUME_UP_BEHAVIOR_MUTE:
                                 return mRingerToggleChord != VOLUME_HUSH_OFF;
@@ -2532,7 +2533,7 @@
                         }
                     }
                     @Override
-                    void execute() {
+                    public void execute() {
                         switch (mPowerVolUpBehavior) {
                             case POWER_VOLUME_UP_BEHAVIOR_MUTE:
                                 // no haptic feedback here since
@@ -2551,7 +2552,7 @@
                         }
                     }
                     @Override
-                    void cancel() {
+                    public void cancel() {
                         switch (mPowerVolUpBehavior) {
                             case POWER_VOLUME_UP_BEHAVIOR_MUTE:
                                 cancelPendingRingerToggleChordAction();
@@ -2567,16 +2568,16 @@
             mKeyCombinationManager.addRule(
                     new TwoKeysCombinationRule(KEYCODE_BACK, KEYCODE_DPAD_DOWN) {
                         @Override
-                        void execute() {
+                        public void execute() {
                             mBackKeyHandled = true;
                             interceptAccessibilityGestureTv();
                         }
                         @Override
-                        void cancel() {
+                        public void cancel() {
                             cancelAccessibilityGestureTv();
                         }
                         @Override
-                        long getKeyInterceptDelayMs() {
+                        public long getKeyInterceptDelayMs() {
                             // Use a timeout of 0 to prevent additional latency in processing of
                             // this key. This will potentially cause some unwanted UI actions if the
                             // user does end up triggering the key combination later, but in most
@@ -2590,16 +2591,16 @@
             mKeyCombinationManager.addRule(
                     new TwoKeysCombinationRule(KEYCODE_DPAD_CENTER, KEYCODE_BACK) {
                         @Override
-                        void execute() {
+                        public void execute() {
                             mBackKeyHandled = true;
                             interceptBugreportGestureTv();
                         }
                         @Override
-                        void cancel() {
+                        public void cancel() {
                             cancelBugreportGestureTv();
                         }
                         @Override
-                        long getKeyInterceptDelayMs() {
+                        public long getKeyInterceptDelayMs() {
                             return 0;
                         }
                     });
@@ -3403,15 +3404,18 @@
                             + keyguardOn() + " canceled=" + event.isCanceled());
         }
 
-        if (mKeyCombinationManager.isKeyConsumed(event)) {
-            return keyConsumed;
-        }
+        if (!useKeyGestureEventHandler()) {
+            if (mKeyCombinationManager.isKeyConsumed(event)) {
+                return keyConsumed;
+            }
 
-        if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
-            final long now = SystemClock.uptimeMillis();
-            final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
-            if (now < interceptTimeout) {
-                return interceptTimeout - now;
+            if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+                final long now = SystemClock.uptimeMillis();
+                final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(
+                        keyCode);
+                if (now < interceptTimeout) {
+                    return interceptTimeout - now;
+                }
             }
         }
 
@@ -3453,8 +3457,16 @@
     // NOTE: Please try not to add new Shortcut combinations here and instead use KeyGestureEvents.
     // Add shortcut trigger logic in {@code KeyGestureController} and add handling logic in
     // {@link handleKeyGesture()}
-    @SuppressLint("MissingPermission")
     private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) {
+        if (useKeyGestureEventHandler()) {
+            return interceptSystemKeysAndShortcutsNew(focusedToken, event);
+        } else {
+            return interceptSystemKeysAndShortcutsOld(focusedToken, event);
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    private boolean interceptSystemKeysAndShortcutsOld(IBinder focusedToken, KeyEvent event) {
         final boolean keyguardOn = keyguardOn();
         final int keyCode = event.getKeyCode();
         final int repeatCount = event.getRepeatCount();
@@ -3878,6 +3890,59 @@
         return (metaState & KeyEvent.META_META_ON) != 0;
     }
 
+    private boolean interceptSystemKeysAndShortcutsNew(IBinder focusedToken, KeyEvent event) {
+        final int keyCode = event.getKeyCode();
+        final int metaState = event.getMetaState();
+        final boolean keyguardOn = keyguardOn();
+
+        if (isUserSetupComplete() && !keyguardOn) {
+            if (mModifierShortcutManager.interceptKey(event)) {
+                dismissKeyboardShortcutsMenu();
+                return true;
+            }
+        }
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_HOME:
+                return handleHomeShortcuts(focusedToken, event);
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+            case KeyEvent.KEYCODE_VOLUME_MUTE:
+                if (mUseTvRouting || mHandleVolumeKeysInWM) {
+                    // On TVs or when the configuration is enabled, volume keys never
+                    // go to the foreground app.
+                    dispatchDirectAudioEvent(event);
+                    return true;
+                }
+
+                // If the device is in VR mode and keys are "internal" (e.g. on the side of the
+                // device), then drop the volume keys and don't forward it to the
+                // application/dispatch the audio event.
+                if (mDefaultDisplayPolicy.isPersistentVrModeEnabled()) {
+                    final InputDevice d = event.getDevice();
+                    if (d != null && !d.isExternal()) {
+                        return true;
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_STEM_PRIMARY:
+                if (prepareToSendSystemKeyToApplication(focusedToken, event)) {
+                    // Send to app.
+                    return false;
+                } else {
+                    // Intercepted.
+                    sendSystemKeyToStatusBarAsync(event);
+                    return true;
+                }
+        }
+        if (isValidGlobalKey(keyCode)
+                && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
+            return true;
+        }
+
+        // Reserve all the META modifier combos for system behavior
+        return (metaState & KeyEvent.META_META_ON) != 0;
+    }
+
     @SuppressLint("MissingPermission")
     private void initKeyGestures() {
         if (!useKeyGestureEventHandler()) {
@@ -3887,7 +3952,13 @@
             @Override
             public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
                     @Nullable IBinder focusedToken) {
-                return PhoneWindowManager.this.handleKeyGestureEvent(event, focusedToken);
+                boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+                        focusedToken);
+                if (handled && Arrays.stream(event.getKeycodes()).anyMatch(
+                        (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+                    mPowerKeyHandled = true;
+                }
+                return handled;
             }
 
             @Override
@@ -3917,7 +3988,20 @@
                     case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
                         return true;
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+                        return mDefaultDisplayPolicy.isAwake();
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+                        return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+                                .isAccessibilityShortcutAvailable(isKeyguardLocked());
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+                        return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+                                .isAccessibilityShortcutAvailable(false);
                     default:
                         return false;
                 }
@@ -4075,6 +4159,66 @@
                     sendSwitchKeyboardLayout(displayId, focusedToken, direction);
                 }
                 return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+                if (start) {
+                    // Screenshot chord is pressed: Wait for long press delay before taking
+                    // screenshot
+                    interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
+                            getScreenshotChordLongPressDelay());
+                } else {
+                    cancelPendingScreenshotChordAction();
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+                if (start) {
+                    interceptAccessibilityShortcutChord();
+                } else {
+                    cancelPendingAccessibilityShortcutAction();
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+                if (start) {
+                    interceptRingerToggleChord();
+                } else {
+                    cancelPendingRingerToggleChordAction();
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+                if (start) {
+                    performHapticFeedback(
+                            HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
+                            "KEY_GESTURE_TYPE_GLOBAL_ACTIONS - Global Actions");
+                    showGlobalActions();
+                } else {
+                    cancelGlobalActionsAction();
+                }
+                return true;
+                // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
+            case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+                if (start) {
+                    interceptAccessibilityGestureTv();
+                } else {
+                    cancelAccessibilityGestureTv();
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+                if (start) {
+                    interceptBugreportGestureTv();
+                } else {
+                    cancelBugreportGestureTv();
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+                if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+                        isKeyguardLocked())) {
+                    mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT));
+                }
+                return true;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+                if (complete) {
+                    mContext.closeSystemDialogs();
+                }
+                return true;
         }
         return false;
     }
@@ -4246,7 +4390,8 @@
         mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
     }
 
-    private void requestBugreportForTv() {
+    @VisibleForTesting
+    void requestBugreportForTv() {
         try {
             if (!ActivityManager.getService().launchBugReportHandlerApp()) {
                 ActivityManager.getService().requestInteractiveBugReport();
@@ -4259,7 +4404,7 @@
     // TODO(b/117479243): handle it in InputPolicy
     /** {@inheritDoc} */
     @Override
-    public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
+    public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
         // Note: This method is only called if the initial down was unhandled.
         if (DEBUG_INPUT) {
             final KeyInterceptionInfo info =
@@ -4272,75 +4417,26 @@
                     + ", keyCode=" + event.getKeyCode()
                     + ", scanCode=" + event.getScanCode()
                     + ", metaState=" + event.getMetaState()
-                    + ", repeatCount=" + event.getRepeatCount()
-                    + ", policyFlags=" + policyFlags);
+                    + ", repeatCount=" + event.getRepeatCount());
         }
 
-        if (interceptUnhandledKey(event, focusedToken)) {
-            return null;
-        }
-
-        KeyEvent fallbackEvent = null;
-        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
-            final KeyCharacterMap kcm = event.getKeyCharacterMap();
-            final int keyCode = event.getKeyCode();
-            final int metaState = event.getMetaState();
-            final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
-                    && event.getRepeatCount() == 0;
-
-            // Check for fallback actions specified by the key character map.
-            final FallbackAction fallbackAction;
-            if (initialDown) {
-                fallbackAction = kcm.getFallbackAction(keyCode, metaState);
-            } else {
-                fallbackAction = mFallbackActions.get(keyCode);
-            }
-
-            if (fallbackAction != null) {
-                if (DEBUG_INPUT) {
-                    Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode
-                            + " metaState=" + Integer.toHexString(fallbackAction.metaState));
-                }
-
-                final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
-                fallbackEvent = KeyEvent.obtain(
-                        event.getDownTime(), event.getEventTime(),
-                        event.getAction(), fallbackAction.keyCode,
-                        event.getRepeatCount(), fallbackAction.metaState,
-                        event.getDeviceId(), event.getScanCode(),
-                        flags, event.getSource(), event.getDisplayId(), null);
-
-                if (!interceptFallback(focusedToken, fallbackEvent, policyFlags)) {
-                    fallbackEvent.recycle();
-                    fallbackEvent = null;
-                }
-
-                if (initialDown) {
-                    mFallbackActions.put(keyCode, fallbackAction);
-                } else if (event.getAction() == KeyEvent.ACTION_UP) {
-                    mFallbackActions.remove(keyCode);
-                    fallbackAction.recycle();
-                }
-            }
-        }
-
-        if (DEBUG_INPUT) {
-            if (fallbackEvent == null) {
-                Slog.d(TAG, "No fallback.");
-            } else {
-                Slog.d(TAG, "Performing fallback: " + fallbackEvent);
-            }
-        }
-        return fallbackEvent;
-    }
-
-    private boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
         final int keyCode = event.getKeyCode();
         final int repeatCount = event.getRepeatCount();
         final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
         final int metaState = event.getModifiers();
 
-        switch(keyCode) {
+        // TODO(b/358569822): Shift to KeyGestureEvent based handling
+        if (keyCode == KeyEvent.KEYCODE_STEM_PRIMARY) {
+            handleUnhandledSystemKey(event);
+            sendSystemKeyToStatusBarAsync(event);
+            return true;
+        }
+
+        if (useKeyGestureEventHandler()) {
+            return false;
+        }
+
+        switch (keyCode) {
             case KeyEvent.KEYCODE_SPACE:
                 if (down && repeatCount == 0) {
                     // Handle keyboard layout switching. (CTRL + SPACE)
@@ -4377,10 +4473,6 @@
                     return true;
                 }
                 break;
-            case KeyEvent.KEYCODE_STEM_PRIMARY:
-                handleUnhandledSystemKey(event);
-                sendSystemKeyToStatusBarAsync(event);
-                return true;
         }
 
         return false;
@@ -4419,19 +4511,6 @@
                 targetWindowToken);
     }
 
-    private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
-            int policyFlags) {
-        int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
-        if ((actions & ACTION_PASS_TO_USER) != 0) {
-            long delayMillis = interceptKeyBeforeDispatching(
-                    focusedToken, fallbackEvent, policyFlags);
-            if (delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @Override
     public void setTopFocusedDisplay(int displayId) {
         mTopFocusedDisplayId = displayId;
@@ -4888,6 +4967,7 @@
         final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
         boolean isWakeKey = (policyFlags & WindowManagerPolicy.FLAG_WAKE) != 0
                 || event.isWakeKey();
+        boolean isKeyGestureTriggered = (policyFlags & FLAG_KEY_GESTURE_TRIGGERED) != 0;
 
         // There are key events that perform the operation as the current user,
         // and these should be ignored for visible background users.
@@ -5014,8 +5094,13 @@
         final boolean isDefaultDisplayOn = Display.isOnState(mDefaultDisplay.getState());
         final boolean isDefaultDisplayAwake = mDefaultDisplayPolicy.isAwake();
         final boolean interactiveAndAwake = interactive && isDefaultDisplayAwake;
-        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
-            handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+        if (isKeyGestureTriggered) {
+            // If key gesture is triggered outside policy, reset gesture handlers here
+            mSingleKeyGestureDetector.reset();
+        } else {
+            if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+                handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+            }
         }
 
         // Enable haptics if down and virtual key without multiple repetitions. If this is a hard
@@ -5178,7 +5263,7 @@
                 result &= ~ACTION_PASS_TO_USER;
                 isWakeKey = false; // wake-up will be handled separately
                 if (down) {
-                    interceptPowerKeyDown(event, interactiveAndAwake);
+                    interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered);
                 } else {
                     interceptPowerKeyUp(event, canceled);
                 }
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 892af6b..ad11657 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -754,11 +754,9 @@
      * @param focusedToken Client window token that currently has focus. This is where the key
      *            event will normally go.
      * @param event The key event.
-     * @param policyFlags The policy flags associated with the key.
-     * @return Returns an alternate key event to redispatch as a fallback, or null to give up.
-     * The caller is responsible for recycling the key event.
+     * @return true if the unhandled key is intercepted by the policy.
      */
-    KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags);
+    boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken);
 
     /**
      * Called when the top focused display is changed.
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 303828f..0cdf537 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -53,6 +53,7 @@
 import android.util.EventLog;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.WindowManagerPolicyConstants;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -512,8 +513,17 @@
             }
 
             // Start input as soon as we start waking up or going to sleep.
-            mInputManagerInternal.setInteractive(interactive);
             mInputMethodManagerInternal.setInteractive(interactive);
+            if (!mFlags.isPerDisplayWakeByTouchEnabled()) {
+                // Since wakefulness is a global property in original logic, all displays should
+                // be set to the same interactive state, matching system's global wakefulness
+                SparseBooleanArray displayInteractivities = new SparseBooleanArray();
+                int[] displayIds = mDisplayManagerInternal.getDisplayIds().toArray();
+                for (int displayId : displayIds) {
+                    displayInteractivities.put(displayId, interactive);
+                }
+                mInputManagerInternal.setDisplayInteractivities(displayInteractivities);
+            }
 
             // Notify battery stats.
             try {
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index c6ef89d..fd60e06 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -42,6 +42,11 @@
             Flags::improveWakelockLatency
     );
 
+    private final FlagState mPerDisplayWakeByTouch = new FlagState(
+            Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH,
+            Flags::perDisplayWakeByTouch
+    );
+
     /** Returns whether early-screen-timeout-detector is enabled on not. */
     public boolean isEarlyScreenTimeoutDetectorEnabled() {
         return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
@@ -55,6 +60,13 @@
     }
 
     /**
+     * @return Whether per-display wake by touch is enabled or not.
+     */
+    public boolean isPerDisplayWakeByTouchEnabled() {
+        return mPerDisplayWakeByTouch.isEnabled();
+    }
+
+    /**
      * dumps all flagstates
      * @param pw printWriter
      */
@@ -62,6 +74,7 @@
         pw.println("PowerManagerFlags:");
         pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
         pw.println(" " + mImproveWakelockLatency);
+        pw.println(" " + mPerDisplayWakeByTouch);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index 3581b2f..9cf3bb6 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -17,4 +17,12 @@
     description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
     bug: "339590565"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+    name: "per_display_wake_by_touch"
+    namespace: "power"
+    description: "Feature flag to enable per-display wake by touch"
+    bug: "343295183"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
new file mode 100644
index 0000000..dd6d5db
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
@@ -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.server.power.stats;
+
+import android.os.BatteryUsageStats;
+import android.util.IndentingPrintWriter;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+class AccumulatedBatteryUsageStatsSection extends PowerStatsSpan.Section {
+    public static final String TYPE = "accumulated-battery-usage-stats";
+    public static final long ID = Long.MAX_VALUE;
+
+    private final BatteryUsageStats.Builder mBatteryUsageStats;
+
+    AccumulatedBatteryUsageStatsSection(BatteryUsageStats.Builder batteryUsageStats) {
+        super(TYPE);
+        mBatteryUsageStats = batteryUsageStats;
+    }
+
+    public BatteryUsageStats.Builder getBatteryUsageStatsBuilder() {
+        return mBatteryUsageStats;
+    }
+
+    @Override
+    public void write(TypedXmlSerializer serializer) throws IOException {
+        mBatteryUsageStats.build().writeXml(serializer);
+    }
+
+    @Override
+    public void dump(IndentingPrintWriter ipw) {
+        mBatteryUsageStats.build().dump(ipw, "");
+    }
+
+    static class Reader implements PowerStatsSpan.SectionReader {
+        @Override
+        public String getType() {
+            return TYPE;
+        }
+
+        @Override
+        public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            return new AccumulatedBatteryUsageStatsSection(
+                    BatteryUsageStats.createBuilderFromXml(parser));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cb8e1a0..3f1d9a3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -509,6 +509,7 @@
     }
 
     private boolean mSaveBatteryUsageStatsOnReset;
+    private boolean mAccumulateBatteryUsageStats;
     private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
     private PowerStatsStore mPowerStatsStore;
 
@@ -11975,10 +11976,12 @@
      */
     public void saveBatteryUsageStatsOnReset(
             @NonNull BatteryUsageStatsProvider batteryUsageStatsProvider,
-            @NonNull PowerStatsStore powerStatsStore) {
+            @NonNull PowerStatsStore powerStatsStore,
+            boolean accumulateBatteryUsageStats) {
         mSaveBatteryUsageStatsOnReset = true;
         mBatteryUsageStatsProvider = batteryUsageStatsProvider;
         mPowerStatsStore = powerStatsStore;
+        mAccumulateBatteryUsageStats = accumulateBatteryUsageStats;
     }
 
     @GuardedBy("this")
@@ -12179,29 +12182,33 @@
             return;
         }
 
-        final BatteryUsageStats batteryUsageStats;
-        synchronized (this) {
-            batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
-                    new BatteryUsageStatsQuery.Builder()
-                            .setMaxStatsAgeMs(0)
-                            .includePowerModels()
-                            .includeProcessStateData()
-                            .build());
-        }
-
-        // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
-        // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
-        // start time
-        long monotonicStartTime =
-                mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
-        mHandler.post(() -> {
-            mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
-            try {
-                batteryUsageStats.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Cannot close BatteryUsageStats", e);
+        if (mAccumulateBatteryUsageStats) {
+            mBatteryUsageStatsProvider.accumulateBatteryUsageStats(this);
+        } else {
+            final BatteryUsageStats batteryUsageStats;
+            synchronized (this) {
+                batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
+                        new BatteryUsageStatsQuery.Builder()
+                                .setMaxStatsAgeMs(0)
+                                .includePowerModels()
+                                .includeProcessStateData()
+                                .build());
             }
-        });
+
+            // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
+            // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
+            // start time
+            long monotonicStartTime =
+                    mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
+            mHandler.post(() -> {
+                mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
+                try {
+                    batteryUsageStats.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Cannot close BatteryUsageStats", e);
+                }
+            });
+        }
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 87a3e5e..d66e05b 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -33,6 +33,7 @@
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.PowerProfile;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -64,6 +65,7 @@
         mClock = clock;
 
         mPowerStatsStore.addSectionReader(new BatteryUsageStatsSection.Reader());
+        mPowerStatsStore.addSectionReader(new AccumulatedBatteryUsageStatsSection.Reader());
     }
 
     private List<PowerCalculator> getPowerCalculators() {
@@ -151,6 +153,56 @@
     }
 
     /**
+     * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
+     * add them to the accumulated stats and save the result.
+     */
+    public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
+        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+
+        PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+                AccumulatedBatteryUsageStatsSection.ID,
+                AccumulatedBatteryUsageStatsSection.TYPE);
+        if (powerStatsSpan != null) {
+            List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+            for (int i = sections.size() - 1; i >= 0; i--) {
+                PowerStatsSpan.Section section = sections.get(i);
+                if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+                    accumulatedBatteryUsageStatsBuilder =
+                            ((AccumulatedBatteryUsageStatsSection) section)
+                                    .getBatteryUsageStatsBuilder();
+                    break;
+                }
+            }
+        }
+
+        // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
+        // builder to avoid allocating a second CursorWindow
+        BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
+                getCurrentBatteryUsageStatsBuilder(stats,
+                        new BatteryUsageStatsQuery.Builder()
+                                .setMaxStatsAgeMs(0)
+                                .includeProcessStateData()
+                                .includePowerStateData()
+                                .includeScreenStateData()
+                                .build(),
+                        mClock.currentTimeMillis());
+
+        if (accumulatedBatteryUsageStatsBuilder == null) {
+            accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
+        } else {
+            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
+            currentBatteryUsageStatsBuilder.discard();
+        }
+
+        powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+        powerStatsSpan.addSection(
+                new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
+
+        mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
+                accumulatedBatteryUsageStatsBuilder::discard);
+    }
+
+    /**
      * Returns true if the last update was too long ago for the tolerances specified
      * by the supplied queries.
      */
@@ -192,15 +244,67 @@
 
     private BatteryUsageStats getBatteryUsageStats(BatteryStatsImpl stats,
             BatteryUsageStatsQuery query, long currentTimeMs) {
-        if (query.getToTimestamp() == 0) {
+        if ((query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
+            return getAccumulatedBatteryUsageStats(stats, query);
+        } else if (query.getToTimestamp() == 0) {
             return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
         } else {
             return getAggregatedBatteryUsageStats(stats, query);
         }
     }
 
+    private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
+            BatteryUsageStatsQuery query) {
+        PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+                AccumulatedBatteryUsageStatsSection.ID,
+                AccumulatedBatteryUsageStatsSection.TYPE);
+
+        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+        if (powerStatsSpan != null) {
+            List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+            if (sections.size() == 1) {
+                accumulatedBatteryUsageStatsBuilder =
+                        ((AccumulatedBatteryUsageStatsSection) sections.get(0))
+                                .getBatteryUsageStatsBuilder();
+            } else {
+                Slog.wtf(TAG, "Unexpected number of sections for type "
+                        + AccumulatedBatteryUsageStatsSection.TYPE);
+            }
+        }
+
+        BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+                mClock.currentTimeMillis());
+
+        BatteryUsageStats result;
+        if (accumulatedBatteryUsageStatsBuilder == null) {
+            result = currentBatteryUsageStats;
+        } else {
+            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
+            try {
+                currentBatteryUsageStats.close();
+            } catch (IOException ex) {
+                Slog.e(TAG, "Closing BatteryUsageStats", ex);
+            }
+            result = accumulatedBatteryUsageStatsBuilder.build();
+        }
+
+        return result;
+    }
+
     private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
             BatteryUsageStatsQuery query, long currentTimeMs) {
+        BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
+                currentTimeMs);
+        BatteryUsageStats batteryUsageStats = builder.build();
+        if (batteryUsageStats.isProcessStateDataIncluded()) {
+            verify(batteryUsageStats);
+        }
+        return batteryUsageStats;
+    }
+
+    private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
+            BatteryUsageStatsQuery query, long currentTimeMs) {
         final long realtimeUs = mClock.elapsedRealtime() * 1000;
         final long uptimeUs = mClock.uptimeMillis() * 1000;
 
@@ -274,11 +378,7 @@
         mPowerAttributor.estimatePowerConsumption(batteryUsageStatsBuilder, stats.getHistory(),
                 monotonicStartTime, monotonicEndTime);
 
-        BatteryUsageStats batteryUsageStats = batteryUsageStatsBuilder.build();
-        if (includeProcessStateData) {
-            verify(batteryUsageStats);
-        }
-        return batteryUsageStats;
+        return batteryUsageStatsBuilder;
     }
 
     // STOPSHIP(b/229906525): remove verification before shipping
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index a875c30..5a6f973 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -133,6 +133,19 @@
     }
 
     /**
+     * Schedules saving the specified span on the background thread.
+     */
+    public void storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete) {
+        mHandler.post(() -> {
+            try {
+                storePowerStatsSpan(span);
+            } finally {
+                onComplete.run();
+            }
+        });
+    }
+
+    /**
      * Saves the specified span in the store.
      */
     public void storePowerStatsSpan(PowerStatsSpan span) {
@@ -172,6 +185,9 @@
         lockStoreDirectory();
         try {
             File file = makePowerStatsSpanFilename(id);
+            if (!file.exists()) {
+                return null;
+            }
             try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                 return PowerStatsSpan.read(inputStream, parser, mSectionReaders, sectionTypes);
             } catch (IOException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 05d29f5..ce6f57f 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -76,3 +76,13 @@
     bug: "364350206"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "accumulate_battery_usage_stats"
+    namespace: "backstage_power"
+    description: "Add support for monotonically accumulated BatteryUsageStats"
+    bug: "345022340"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
index f047f56..ab630ee 100644
--- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
@@ -17,6 +17,7 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -63,6 +64,7 @@
     private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger";
     private static final String SUBSYSTEM_SENSOR_STRING = "Sensor";
     private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data";
+    private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth";
     private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution";
 
     private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
@@ -512,6 +514,8 @@
                 return CPU_WAKEUP_SUBSYSTEM_SENSOR;
             case SUBSYSTEM_CELLULAR_DATA_STRING:
                 return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+            case SUBSYSTEM_BLUETOOTH_STRING:
+                return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
         }
         return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
     }
@@ -528,6 +532,8 @@
                 return SUBSYSTEM_SENSOR_STRING;
             case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
                 return SUBSYSTEM_CELLULAR_DATA_STRING;
+            case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH:
+                return SUBSYSTEM_BLUETOOTH_STRING;
             case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
                 return "Unknown";
         }
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index b35a0a7..74c1124e 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -216,13 +216,13 @@
 import com.android.server.BinderCallsStatsService;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
-import com.android.server.PinnerService.PinnedFileStats;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.am.MemoryStatUtil.MemoryStat;
 import com.android.server.health.HealthServiceWrapper;
 import com.android.server.notification.NotificationManagerService;
+import com.android.server.pinner.PinnerService;
+import com.android.server.pinner.PinnerService.PinnedFileStats;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.power.stats.KernelWakelockReader;
 import com.android.server.power.stats.KernelWakelockStats;
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index 374dd89..6405353 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -37,12 +37,14 @@
 import android.app.timedetector.TelephonyTimeSuggestion;
 import android.content.Context;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.server.SystemClockTime;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timezonedetector.ArrayMapWithHistory;
@@ -315,6 +317,17 @@
         // detected time if, for example, the age of all suggestions are considered.
         NetworkTimeSuggestion lastNetworkSuggestion = mLastNetworkSuggestion.get();
         if (lastNetworkSuggestion == null || !lastNetworkSuggestion.equals(suggestion)) {
+            if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                    && android.os.Flags.networkTimeUsesSharedMemory()) {
+                UnixEpochTime networkUnixEpochTime = suggestion.getUnixEpochTime();
+                long lastNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis =
+                        networkUnixEpochTime.getUnixEpochTimeMillis()
+                                - networkUnixEpochTime.getElapsedRealtimeMillis();
+                ApplicationSharedMemory.getInstance()
+                        .setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+                                lastNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis);
+            }
+
             mLastNetworkSuggestion.set(suggestion);
             notifyNetworkTimeUpdateListenersAsynchronously();
         }
@@ -347,8 +360,12 @@
 
     @Override
     public synchronized void clearLatestNetworkSuggestion() {
+        if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                && android.os.Flags.networkTimeUsesSharedMemory()) {
+            ApplicationSharedMemory.getInstance()
+                    .clearLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+        }
         mLastNetworkSuggestion.set(null);
-
         notifyNetworkTimeUpdateListenersAsynchronously();
 
         // The loss of network time may change the time signal to use to set the system clock.
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index b4d3862..b5a7fcb 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.os.ExternalVibration;
@@ -24,6 +25,7 @@
 import android.os.VibrationAttributes;
 import android.os.vibrator.Flags;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
 
 /**
@@ -32,14 +34,16 @@
 final class ExternalVibrationSession extends Vibration
         implements VibrationSession, IBinder.DeathRecipient {
 
+    private final Object mLock = new Object();
     private final ExternalVibration mExternalVibration;
     private final ExternalVibrationScale mScale = new ExternalVibrationScale();
 
+    @GuardedBy("mLock")
     @Nullable
     private Runnable mBinderDeathCallback;
 
     ExternalVibrationSession(ExternalVibration externalVibration) {
-        super(externalVibration.getToken(), new CallerInfo(
+        super(new CallerInfo(
                 externalVibration.getVibrationAttributes(), externalVibration.getUid(),
                 // TODO(b/249785241): Find a way to link ExternalVibration to a VirtualDevice
                 // instead of using DEVICE_ID_INVALID here and relying on the UID checks.
@@ -82,24 +86,25 @@
     }
 
     @Override
-    public void linkToDeath(Runnable callback) {
-        synchronized (this) {
+    public boolean linkToDeath(Runnable callback) {
+        synchronized (mLock) {
             mBinderDeathCallback = callback;
         }
         mExternalVibration.linkToDeath(this);
+        return true;
     }
 
     @Override
     public void unlinkToDeath() {
         mExternalVibration.unlinkToDeath(this);
-        synchronized (this) {
+        synchronized (mLock) {
             mBinderDeathCallback = null;
         }
     }
 
     @Override
     public void binderDied() {
-        synchronized (this) {
+        synchronized (mLock) {
             if (mBinderDeathCallback != null) {
                 mBinderDeathCallback.run();
             }
@@ -119,9 +124,11 @@
     }
 
     @Override
-    public void notifyEnded() {
+    public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+            boolean immediate) {
         // Notify external client that this vibration should stop sending data to the vibrator.
         mExternalVibration.mute();
+        end(new EndInfo(status, endedBy));
     }
 
     boolean isHoldingSameVibration(ExternalVibration vib) {
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index ea4bd01..ce9c47b 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -36,6 +36,7 @@
 final class HalVibration extends Vibration {
 
     public final SparseArray<VibrationEffect> mFallbacks = new SparseArray<>();
+    public final IBinder callerToken;
 
     /** A {@link CountDownLatch} to enable waiting for completion. */
     private final CountDownLatch mCompletionLatch = new CountDownLatch(1);
@@ -55,9 +56,10 @@
     private int mScaleLevel;
     private float mAdaptiveScale;
 
-    HalVibration(@NonNull IBinder token, @NonNull CombinedVibration effect,
+    HalVibration(@NonNull IBinder callerToken, @NonNull CombinedVibration effect,
             @NonNull VibrationSession.CallerInfo callerInfo) {
-        super(token, callerInfo);
+        super(callerInfo);
+        this.callerToken = callerToken;
         mOriginalEffect = effect;
         mEffectToPlay = effect;
         mScaleLevel = VibrationScaler.SCALE_NONE;
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 9a04793..21fd4ce 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -53,16 +53,13 @@
     public final long id;
     public final VibrationSession.CallerInfo callerInfo;
     public final VibrationStats stats = new VibrationStats();
-    public final IBinder callerToken;
 
     private VibrationSession.Status mStatus;
 
-    Vibration(@NonNull IBinder token, @NonNull VibrationSession.CallerInfo callerInfo) {
-        Objects.requireNonNull(token);
+    Vibration(@NonNull VibrationSession.CallerInfo callerInfo) {
         Objects.requireNonNull(callerInfo);
         mStatus = VibrationSession.Status.RUNNING;
         this.id = sNextVibrationId.getAndIncrement();
-        this.callerToken = token;
         this.callerInfo = callerInfo;
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index 5640b49..70477a2 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -49,14 +49,26 @@
      * Links this session to the app process death with given callback to handle it.
      *
      * <p>This can be used by the service to end the vibration session when the app process dies.
+     *
+     * @param callback The service callback to be triggered when the binder dies
+     * @return true if the link was successful, false otherwise
      */
-    void linkToDeath(Runnable callback);
+    boolean linkToDeath(@Nullable Runnable callback);
 
     /** Removes link to the app process death. */
     void unlinkToDeath();
 
-    /** Notify the session end was requested, which might be acted upon asynchronously. */
-    void notifyEnded();
+    /**
+     * Notify the session end was requested, which might be acted upon asynchronously.
+     *
+     * <p>Only the first end signal will be used to end a session, but subsequent calls with
+     * {@code immediate} flag set to true can still force it to take effect urgently.
+     *
+     * @param status the end status.
+     * @param endedBy the {@link CallerInfo} of the session that requested this session to end.
+     * @param immediate indicates whether cancellation should abort urgently and skip cleanup steps.
+     */
+    void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, boolean immediate);
 
     /**
      * Session status with reference to values from vibratormanagerservice.proto for logging.
@@ -119,7 +131,7 @@
         public final String reason;
 
         CallerInfo(@NonNull VibrationAttributes attrs, int uid, int deviceId, String opPkg,
-                String reason) {
+                @Nullable String reason) {
             Objects.requireNonNull(attrs);
             this.attrs = attrs;
             this.uid = uid;
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 69cdcf4..9cb8c1a 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -31,6 +31,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.IActivityManager;
 import android.app.SynchronousUserSwitchObserver;
 import android.app.UidObserver;
 import android.content.BroadcastReceiver;
@@ -74,6 +75,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /** Controls all the system settings related to vibration. */
@@ -147,9 +149,6 @@
                     PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE,
                     PowerManager.GO_TO_SLEEP_REASON_TIMEOUT));
 
-    private static final IntentFilter INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER =
-            new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
-
     /** Listener for changes on vibration settings. */
     interface OnVibratorSettingsChanged {
         /** Callback triggered when any of the vibrator settings change. */
@@ -158,15 +157,18 @@
 
     private final Object mLock = new Object();
     private final Context mContext;
-    private final String mSystemUiPackage;
     @VisibleForTesting
     final SettingsContentObserver mSettingObserver;
     @VisibleForTesting
-    final SettingsBroadcastReceiver mSettingChangeReceiver;
+    final RingerModeBroadcastReceiver mRingerModeBroadcastReceiver;
+    @VisibleForTesting
+    final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
     @VisibleForTesting
     final VibrationUidObserver mUidObserver;
     @VisibleForTesting
     final VibrationUserSwitchObserver mUserSwitchObserver;
+    @VisibleForTesting
+    final VibrationLowPowerModeListener mLowPowerModeListener;
 
     @GuardedBy("mLock")
     private final List<OnVibratorSettingsChanged> mListeners = new ArrayList<>();
@@ -180,10 +182,13 @@
     @GuardedBy("mLock")
     @Nullable
     private PowerManagerInternal mPowerManagerInternal;
+    @GuardedBy("mLock")
     @Nullable
     private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
 
     @GuardedBy("mLock")
+    private String mSystemUiPackage;
+    @GuardedBy("mLock")
     private boolean mVibrateInputDevices;
     @GuardedBy("mLock")
     private SparseIntArray mCurrentVibrationIntensities = new SparseIntArray();
@@ -205,11 +210,11 @@
         mContext = context;
         mVibrationConfig = config;
         mSettingObserver = new SettingsContentObserver(handler);
-        mSettingChangeReceiver = new SettingsBroadcastReceiver();
+        mRingerModeBroadcastReceiver = new RingerModeBroadcastReceiver();
+        mBatteryBroadcastReceiver = new BatteryBroadcastReceiver();
         mUidObserver = new VibrationUidObserver();
         mUserSwitchObserver = new VibrationUserSwitchObserver();
-        mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
-                .getSystemUiServiceComponent().getPackageName();
+        mLowPowerModeListener = new VibrationLowPowerModeListener();
 
         VibrationEffect clickEffect = createEffectFromResource(
                 com.android.internal.R.array.config_virtualKeyVibePattern);
@@ -233,18 +238,34 @@
     }
 
     public void onSystemReady() {
-        PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class);
-        AudioManager am = mContext.getSystemService(AudioManager.class);
-        int ringerMode = am.getRingerModeInternal();
+        onSystemReady(LocalServices.getService(PackageManagerInternal.class),
+                LocalServices.getService(PowerManagerInternal.class),
+                ActivityManager.getService(),
+                LocalServices.getService(VirtualDeviceManagerInternal.class),
+                mContext.getSystemService(AudioManager.class));
+    }
+
+    @VisibleForTesting
+    void onSystemReady(PackageManagerInternal packageManagerInternal,
+            PowerManagerInternal powerManagerInternal,
+            IActivityManager activityManagerInternal,
+            @Nullable VirtualDeviceManagerInternal virtualDeviceManagerInternal,
+            @Nullable AudioManager audioManager) {
+        int ringerMode = (audioManager == null)
+                ? AudioManager.RINGER_MODE_NORMAL
+                : audioManager.getRingerModeInternal();
+        String sysUiPackage = packageManagerInternal.getSystemUiServiceComponent().getPackageName();
 
         synchronized (mLock) {
-            mPowerManagerInternal = pm;
-            mAudioManager = am;
+            mPowerManagerInternal = powerManagerInternal;
+            mVirtualDeviceManagerInternal = virtualDeviceManagerInternal;
+            mAudioManager = audioManager;
             mRingerMode = ringerMode;
+            mSystemUiPackage = sysUiPackage;
         }
 
         try {
-            ActivityManager.getService().registerUidObserver(mUidObserver,
+            activityManagerInternal.registerUidObserver(mUidObserver,
                     ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
                     ActivityManager.PROCESS_STATE_UNKNOWN, /* callingPackage= */ null);
         } catch (RemoteException e) {
@@ -252,32 +273,16 @@
         }
 
         try {
-            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
+            activityManagerInternal.registerUserSwitchObserver(mUserSwitchObserver, TAG);
         } catch (RemoteException e) {
             // ignored; both services live in system_server
         }
 
-        pm.registerLowPowerModeObserver(
-                new PowerManagerInternal.LowPowerModeListener() {
-                    @Override
-                    public int getServiceType() {
-                        return PowerManager.ServiceType.VIBRATION;
-                    }
+        powerManagerInternal.registerLowPowerModeObserver(mLowPowerModeListener);
 
-                    @Override
-                    public void onLowPowerModeChanged(PowerSaveState result) {
-                        boolean shouldNotifyListeners;
-                        synchronized (mLock) {
-                            shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode;
-                            mBatterySaverMode = result.batterySaverEnabled;
-                        }
-                        if (shouldNotifyListeners) {
-                            notifyListeners();
-                        }
-                    }
-                });
-
-        registerSettingsChangeReceiver(INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER);
+        mContext.registerReceiver(mRingerModeBroadcastReceiver,
+                new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION),
+                Context.RECEIVER_EXPORTED_UNAUDITED);
 
         // Listen to all settings that might affect the result of Vibrator.getVibrationIntensity.
         registerSettingsObserver(Settings.System.getUriFor(Settings.System.VIBRATE_INPUT_DEVICES));
@@ -301,12 +306,7 @@
 
         if (mVibrationConfig.ignoreVibrationsOnWirelessCharger()) {
             Intent batteryStatus = mContext.registerReceiver(
-                    new BroadcastReceiver() {
-                        @Override
-                        public void onReceive(Context context, Intent intent) {
-                            updateBatteryInfo(intent);
-                        }
-                    },
+                    mBatteryBroadcastReceiver,
                     new IntentFilter(Intent.ACTION_BATTERY_CHANGED),
                     Context.RECEIVER_NOT_EXPORTED);
             // After registering the receiver for battery status, process the sticky broadcast that
@@ -476,8 +476,10 @@
     public boolean shouldCancelVibrationOnScreenOff(@NonNull CallerInfo callerInfo,
             long vibrationStartUptimeMillis) {
         PowerManagerInternal pm;
+        String sysUiPackageName;
         synchronized (mLock) {
             pm = mPowerManagerInternal;
+            sysUiPackageName = mSystemUiPackage;
         }
         if (pm != null) {
             // The SleepData from PowerManager may refer to a more recent sleep than the broadcast
@@ -501,7 +503,7 @@
         }
         // Only allow vibrations from System packages to continue vibrating when the screen goes off
         return callerInfo.uid != Process.SYSTEM_UID && callerInfo.uid != 0
-                && !mSystemUiPackage.equals(callerInfo.opPkg);
+                && !Objects.equals(sysUiPackageName, callerInfo.opPkg);
     }
 
     /**
@@ -782,11 +784,6 @@
                 UserHandle.USER_ALL);
     }
 
-    private void registerSettingsChangeReceiver(IntentFilter intentFilter) {
-        mContext.registerReceiver(mSettingChangeReceiver, intentFilter,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
-    }
-
     @Nullable
     private VibrationEffect createEffectFromResource(int resId) {
         return createEffectFromResource(mContext.getResources(), resId);
@@ -833,12 +830,11 @@
     }
 
     private boolean isAppRunningOnAnyVirtualDevice(int uid) {
-        if (mVirtualDeviceManagerInternal == null) {
-            mVirtualDeviceManagerInternal =
-                    LocalServices.getService(VirtualDeviceManagerInternal.class);
+        VirtualDeviceManagerInternal vdm;
+        synchronized (mLock) {
+            vdm = mVirtualDeviceManagerInternal;
         }
-        return mVirtualDeviceManagerInternal != null
-                && mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(uid);
+        return vdm != null && vdm.isAppRunningOnAnyVirtualDevice(uid);
     }
 
     /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
@@ -857,7 +853,7 @@
 
     /** Implementation of {@link BroadcastReceiver} to update on ringer mode change. */
     @VisibleForTesting
-    final class SettingsBroadcastReceiver extends BroadcastReceiver {
+    final class RingerModeBroadcastReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
@@ -868,6 +864,18 @@
         }
     }
 
+    /** Implementation of {@link BroadcastReceiver} to update on battery mode change. */
+    @VisibleForTesting
+    final class BatteryBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+                updateBatteryInfo(intent);
+            }
+        }
+    }
+
     /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
     @VisibleForTesting
     final class VibrationUidObserver extends UidObserver {
@@ -913,4 +921,25 @@
             update();
         }
     }
+
+    /** Implementation of {@link PowerManagerInternal.LowPowerModeListener} for low battery. */
+    @VisibleForTesting
+    final class VibrationLowPowerModeListener implements PowerManagerInternal.LowPowerModeListener {
+        @Override
+        public int getServiceType() {
+            return PowerManager.ServiceType.VIBRATION;
+        }
+
+        @Override
+        public void onLowPowerModeChanged(PowerSaveState result) {
+            boolean shouldNotifyListeners;
+            synchronized (mLock) {
+                shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode;
+                mBatterySaverMode = result.batterySaverEnabled;
+            }
+            if (shouldNotifyListeners) {
+                notifyListeners();
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 5137d19..1d52e3c 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,6 +21,7 @@
 import android.os.Build;
 import android.os.CombinedVibration;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.VibrationEffect;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
@@ -38,6 +39,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
 import java.util.Queue;
 import java.util.concurrent.CancellationException;
@@ -358,6 +360,28 @@
     }
 
     /**
+     * Returns true if successfully linked this conductor to the death of the binder that requested
+     * the vibration.
+     */
+    public boolean linkToDeath() {
+        try {
+            mVibration.callerToken.linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error linking vibration to token death", e);
+            return false;
+        }
+        return true;
+    }
+
+    public void unlinkToDeath() {
+        try {
+            mVibration.callerToken.unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            Slog.wtf(TAG, "Failed to unlink vibration to token death", e);
+        }
+    }
+
+    /**
      * Notify the execution that cancellation is requested. This will be acted upon
      * asynchronously in the VibrationThread.
      *
@@ -452,6 +476,23 @@
         }
     }
 
+    /**
+     * Notify that the VibrationThread has completed the vibration effect playback.
+     *
+     * <p>This is a lightweight method intended to be called by the vibration thread directly. The
+     * VibrationThread may still be continuing with cleanup tasks, and should not be given new work
+     * until it notifies the manager that it has been released.
+     */
+    public void notifyVibrationComplete(@NonNull Vibration.EndInfo endInfo) {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(true);
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "Vibration " + mVibration.id + " finished with " + endInfo);
+        }
+        mVibration.end(endInfo);
+    }
+
     /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
     public boolean wasNotifiedToCancel() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 4c1e16c..5b22c10 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,12 +16,12 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.WorkSource;
@@ -31,7 +31,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.vibrator.VibrationSession.Status;
 
-import java.util.NoSuchElementException;
 import java.util.Objects;
 
 /** Plays a {@link HalVibration} in dedicated thread. */
@@ -72,14 +71,6 @@
         void noteVibratorOff(int uid);
 
         /**
-         * Tell the manager that the currently active vibration has completed its vibration, from
-         * the perspective of the Effect. However, the VibrationThread may still be continuing with
-         * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased}
-         * is called.
-         */
-        void onVibrationCompleted(long vibrationId, @NonNull Vibration.EndInfo vibrationEndInfo);
-
-        /**
          * Tells the manager that the VibrationThread is finished with the previous vibration and
          * all of its cleanup tasks, and the vibrators can now be used for another vibration.
          */
@@ -128,7 +119,7 @@
      *  before the release callback.
      */
     boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
         try {
             synchronized (mLock) {
                 if (mRequestedActiveConductor != null) {
@@ -140,7 +131,7 @@
             }
             return true;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -243,7 +234,7 @@
         mWakeLock.acquire();
         try {
             try {
-                runCurrentVibrationWithWakeLockAndDeathLink();
+                playVibration();
             } finally {
                 clientVibrationCompleteIfNotAlready(
                         new Vibration.EndInfo(Status.FINISHED_UNEXPECTED));
@@ -254,46 +245,23 @@
         }
     }
 
-    /**
-     * Runs the VibrationThread with the binder death link, handling link/unlink failures.
-     * Called from within runWithWakeLock.
-     */
-    private void runCurrentVibrationWithWakeLockAndDeathLink() {
-        IBinder vibrationBinderToken = mExecutingConductor.getVibration().callerToken;
-        try {
-            vibrationBinderToken.linkToDeath(mExecutingConductor, 0);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Error linking vibration to token death", e);
-            clientVibrationCompleteIfNotAlready(
-                    new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN));
-            return;
-        }
-        // Ensure that the unlink always occurs now.
-        try {
-            // This is the actual execution of the vibration.
-            playVibration();
-        } finally {
-            try {
-                vibrationBinderToken.unlinkToDeath(mExecutingConductor, 0);
-            } catch (NoSuchElementException e) {
-                Slog.wtf(TAG, "Failed to unlink token", e);
-            }
-        }
-    }
-
     // Indicate that the vibration is complete. This can be called multiple times only for
     // convenience of handling error conditions - an error after the client is complete won't
     // affect the status.
     private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
         if (!mCalledVibrationCompleteCallback) {
             mCalledVibrationCompleteCallback = true;
-            mVibratorManagerHooks.onVibrationCompleted(
-                    mExecutingConductor.getVibration().id, vibrationEndInfo);
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+            try {
+                mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
+            }
         }
     }
 
     private void playVibration() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
         try {
             mExecutingConductor.prepareToStart();
             while (!mExecutingConductor.isFinished()) {
@@ -317,7 +285,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 3c47850..6aed00e 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.Nullable;
 import android.hardware.vibrator.IVibrator;
 import android.os.Binder;
@@ -55,8 +57,7 @@
     // for a snippet of the current known vibrator state/info.
     private volatile VibratorInfo mVibratorInfo;
     private volatile boolean mVibratorInfoLoadSuccessful;
-    private volatile boolean mIsVibrating;
-    private volatile boolean mIsUnderExternalControl;
+    private volatile VibratorState mCurrentState;
     private volatile float mCurrentAmplitude;
 
     /**
@@ -73,6 +74,11 @@
         void onComplete(int vibratorId, long vibrationId);
     }
 
+    /** Representation of the vibrator state based on the interactions through this controller. */
+    private enum VibratorState {
+        IDLE, VIBRATING, UNDER_EXTERNAL_CONTROL
+    }
+
     VibratorController(int vibratorId, OnVibrationCompleteListener listener) {
         this(vibratorId, listener, new NativeWrapper());
     }
@@ -85,6 +91,7 @@
         VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId);
         mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder);
         mVibratorInfo = vibratorInfoBuilder.build();
+        mCurrentState = VibratorState.IDLE;
 
         if (!mVibratorInfoLoadSuccessful) {
             Slog.e(TAG,
@@ -104,7 +111,7 @@
                     return false;
                 }
                 // Notify its callback after new client registered.
-                notifyStateListener(listener, mIsVibrating);
+                notifyStateListener(listener, isVibrating(mCurrentState));
             }
             return true;
         } finally {
@@ -124,7 +131,7 @@
 
     /** Reruns the query to the vibrator to load the {@link VibratorInfo}, if not yet successful. */
     public void reloadVibratorInfoIfNeeded() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
         try {
             // Early check outside lock, for quick return.
             if (mVibratorInfoLoadSuccessful) {
@@ -143,7 +150,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -164,7 +171,7 @@
      * automatically notified to any registered {@link IVibratorStateListener} on change.
      */
     public boolean isVibrating() {
-        return mIsVibrating;
+        return isVibrating(mCurrentState);
     }
 
     /**
@@ -182,11 +189,6 @@
         return mCurrentAmplitude;
     }
 
-    /** Return {@code true} if this vibrator is under external control, false otherwise. */
-    public boolean isUnderExternalControl() {
-        return mIsUnderExternalControl;
-    }
-
     /**
      * Check against this vibrator capabilities.
      *
@@ -199,33 +201,37 @@
 
     /** Return {@code true} if the underlying vibrator is currently available, false otherwise. */
     public boolean isAvailable() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
         try {
             synchronized (mLock) {
                 return mNativeWrapper.isAvailable();
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
     /**
      * Set the vibrator control to be external or not, based on given flag.
      *
-     * <p>This will affect the state of {@link #isUnderExternalControl()}.
+     * <p>This will affect the state of {@link #isVibrating()}.
      */
     public void setExternalControl(boolean externalControl) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setExternalControl(" + externalControl + ")");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR,
+                externalControl ? "VibratorController#enableExternalControl"
+                : "VibratorController#disableExternalControl");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
                 return;
             }
+            VibratorState newState =
+                    externalControl ? VibratorState.UNDER_EXTERNAL_CONTROL : VibratorState.IDLE;
             synchronized (mLock) {
-                mIsUnderExternalControl = externalControl;
                 mNativeWrapper.setExternalControl(externalControl);
+                updateStateAndNotifyListenersLocked(newState);
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -234,7 +240,7 @@
      * if given {@code effect} is {@code null}.
      */
     public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
                 return;
@@ -248,24 +254,24 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
     /** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */
     public void setAmplitude(float amplitude) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
         try {
             synchronized (mLock) {
                 if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
                     mNativeWrapper.setAmplitude(amplitude);
                 }
-                if (mIsVibrating) {
+                if (mCurrentState == VibratorState.VIBRATING) {
                     mCurrentAmplitude = amplitude;
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -279,18 +285,18 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(long milliseconds, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on");
         try {
             synchronized (mLock) {
                 long duration = mNativeWrapper.on(milliseconds, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -304,7 +310,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
         synchronized (mLock) {
             Parcel vendorData = Parcel.obtain();
             try {
@@ -315,12 +321,12 @@
                         vendorEffect.getAdaptiveScale(), vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             } finally {
                 vendorData.recycle();
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
     }
@@ -335,19 +341,19 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(PrebakedSegment prebaked, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
         try {
             synchronized (mLock) {
                 long duration = mNativeWrapper.perform(prebaked.getEffectId(),
                         prebaked.getEffectStrength(), vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -361,7 +367,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(PrimitiveSegment[] primitives, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
                 return 0;
@@ -370,12 +376,12 @@
                 long duration = mNativeWrapper.compose(primitives, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -388,7 +394,7 @@
      * @return The duration of the effect playing, or 0 if unsupported.
      */
     public long on(RampSegment[] primitives, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
                 return 0;
@@ -398,12 +404,12 @@
                 long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -413,15 +419,15 @@
      * <p>This will affect the state of {@link #isVibrating()}.
      */
     public void off() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#off");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#off");
         try {
             synchronized (mLock) {
                 mNativeWrapper.off();
                 mCurrentAmplitude = 0;
-                notifyListenerOnVibrating(false);
+                updateStateAndNotifyListenersLocked(VibratorState.IDLE);
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -439,9 +445,8 @@
         return "VibratorController{"
                 + "mVibratorInfo=" + mVibratorInfo
                 + ", mVibratorInfoLoadSuccessful=" + mVibratorInfoLoadSuccessful
-                + ", mIsVibrating=" + mIsVibrating
+                + ", mCurrentState=" + mCurrentState.name()
                 + ", mCurrentAmplitude=" + mCurrentAmplitude
-                + ", mIsUnderExternalControl=" + mIsUnderExternalControl
                 + ", mVibratorStateListeners count="
                 + mVibratorStateListeners.getRegisteredCallbackCount()
                 + '}';
@@ -450,8 +455,7 @@
     void dump(IndentingPrintWriter pw) {
         pw.println("Vibrator (id=" + mVibratorInfo.getId() + "):");
         pw.increaseIndent();
-        pw.println("isVibrating = " + mIsVibrating);
-        pw.println("isUnderExternalControl = " + mIsUnderExternalControl);
+        pw.println("currentState = " + mCurrentState.name());
         pw.println("currentAmplitude = " + mCurrentAmplitude);
         pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful);
         pw.println("vibratorStateListener size = "
@@ -460,14 +464,19 @@
         pw.decreaseIndent();
     }
 
+    /**
+     * Updates current vibrator state and notify listeners if {@link #isVibrating()} result changed.
+     */
     @GuardedBy("mLock")
-    private void notifyListenerOnVibrating(boolean isVibrating) {
-        if (mIsVibrating != isVibrating) {
-            mIsVibrating = isVibrating;
+    private void updateStateAndNotifyListenersLocked(VibratorState state) {
+        boolean previousIsVibrating = isVibrating(mCurrentState);
+        final boolean newIsVibrating = isVibrating(state);
+        mCurrentState = state;
+        if (previousIsVibrating != newIsVibrating) {
             // The broadcast method is safe w.r.t. register/unregister listener methods, but lock
             // is required here to guarantee delivery order.
             mVibratorStateListeners.broadcast(
-                    listener -> notifyStateListener(listener, isVibrating));
+                    listener -> notifyStateListener(listener, newIsVibrating));
         }
     }
 
@@ -479,6 +488,11 @@
         }
     }
 
+    /** Returns true only if given state is not {@link VibratorState#IDLE}. */
+    private static boolean isVibrating(VibratorState state) {
+        return state != VibratorState.IDLE;
+    }
+
     /** Wrapper around the static-native methods of {@link VibratorController} for tests. */
     @VisibleForTesting
     public static class NativeWrapper {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index a76d8d6..07473d1 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
 import static android.os.VibrationAttributes.USAGE_CLASS_ALARM;
 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -333,7 +334,7 @@
     @VisibleForTesting
     void systemReady() {
         Slog.v(TAG, "Initializing VibratorManager service...");
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "systemReady");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "systemReady");
         try {
             // Will retry to load each vibrator's info, if any request have failed.
             for (int i = 0; i < mVibrators.size(); i++) {
@@ -352,7 +353,7 @@
                 mServiceReady = true;
             }
             Slog.v(TAG, "VibratorManager service initialized");
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -413,7 +414,7 @@
     @Override // Binder call
     public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
             @Nullable CombinedVibration effect, @Nullable VibrationAttributes attrs) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
         try {
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.VIBRATE_ALWAYS_ON,
@@ -449,20 +450,25 @@
             }
             return true;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
     @Override // Binder call
     public void vibrate(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect,
             @Nullable VibrationAttributes attrs, String reason, IBinder token) {
-        vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
+        try {
+            vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
     }
 
     @Override // Binder call
     public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
             String reason, int flags, int privFlags) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
         // Note that the `performHapticFeedback` method does not take a token argument from the
         // caller, and instead, uses this service as the token. This is to mitigate performance
         // impact that would otherwise be caused due to marshal latency. Haptic feedback effects are
@@ -471,7 +477,7 @@
             performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */
                     this, flags, privFlags);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -479,13 +485,13 @@
     public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
             int constant, int inputDeviceId, int inputSource, String reason, int flags,
             int privFlags) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
         try {
             performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant,
                     inputDeviceId,
                     inputSource, reason, /* token= */ this, flags, privFlags);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -563,26 +569,16 @@
     HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
             String reason, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
-        try {
-            attrs = fixupVibrationAttributes(attrs, effect);
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.VIBRATE, "vibrate");
-            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
+        attrs = fixupVibrationAttributes(attrs, effect);
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.VIBRATE, "vibrate");
+        return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
     }
 
     HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
             String reason, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
-        try {
-            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
+        return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
     }
 
     private HalVibration vibrateInternal(int uid, int deviceId, String opPkg,
@@ -633,12 +629,9 @@
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     if (mCurrentExternalVibration != null) {
-                        mCurrentExternalVibration.notifyEnded();
                         vib.stats.reportInterruptedAnotherVibration(
-                                mCurrentExternalVibration.callerInfo);
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
-                                        vib.callerInfo),
+                                mCurrentExternalVibration.getCallerInfo());
+                        endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, vib.callerInfo,
                                 /* continueExternalControl= */ false);
                     } else if (mCurrentVibration != null) {
                         if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
@@ -666,7 +659,7 @@
 
             // Ignored or failed to start the vibration, end it and report metrics right away.
             if (vibrationEndInfo != null) {
-                endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
+                endVibrationLocked(vib, vibrationEndInfo);
             }
             return vib;
         }
@@ -674,7 +667,7 @@
 
     @Override // Binder call
     public void cancelVibrate(int usageFilter, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelVibrate");
         try {
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.VIBRATE,
@@ -703,16 +696,15 @@
                             && shouldCancelVibration(
                             mCurrentExternalVibration.getCallerInfo().attrs,
                             usageFilter)) {
-                        mCurrentExternalVibration.notifyEnded();
-                        endExternalVibrateLocked(
-                                cancelledByUserInfo, /* continueExternalControl= */ false);
+                        endExternalVibrateLocked(cancelledByUserInfo.status,
+                                cancelledByUserInfo.endedBy, /* continueExternalControl= */ false);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -817,17 +809,9 @@
                 mCurrentExternalVibration.getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION);
             }
-
-            boolean isVibrating = false;
-            boolean isUnderExternalControl = false;
             for (int i = 0; i < mVibrators.size(); i++) {
                 proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i));
-                isVibrating |= mVibrators.valueAt(i).isVibrating();
-                isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl();
             }
-            proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating);
-            proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL,
-                    isUnderExternalControl);
         }
         mVibratorManagerRecords.dump(proto);
         mVibratorControlService.dump(proto);
@@ -903,7 +887,7 @@
     @GuardedBy("mLock")
     @Nullable
     private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked");
         try {
             if (mInputDeviceDelegate.isAvailable()) {
                 return startVibrationOnInputDevicesLocked(vib);
@@ -923,7 +907,7 @@
             mNextVibration = conductor;
             return null;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -934,9 +918,14 @@
         int mode = startAppOpModeLocked(vib.callerInfo);
         switch (mode) {
             case AppOpsManager.MODE_ALLOWED:
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+                Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
                 // Make sure mCurrentVibration is set while triggering the VibrationThread.
                 mCurrentVibration = conductor;
+                if (!mCurrentVibration.linkToDeath()) {
+                    // Shouldn't happen. The method call already logs a wtf.
+                    mCurrentVibration = null;  // Aborted.
+                    return new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN);
+                }
                 if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) {
                     // Shouldn't happen. The method call already logs a wtf.
                     mCurrentVibration = null;  // Aborted.
@@ -953,14 +942,21 @@
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo,
-            boolean shouldWriteStats) {
+    private void endVibrationLocked(Vibration vib, Status status) {
+        endVibrationLocked(vib, new Vibration.EndInfo(status));
+    }
+
+    @GuardedBy("mLock")
+    private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo) {
         vib.end(vibrationEndInfo);
+        reportEndedVibrationLocked(vib);
+    }
+
+    @GuardedBy("mLock")
+    private void reportEndedVibrationLocked(Vibration vib) {
         logAndRecordVibration(vib.getDebugInfo());
-        if (shouldWriteStats) {
-            mFrameworkStatsLogger.writeVibrationReportedAsync(
-                    vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
-        }
+        mFrameworkStatsLogger.writeVibrationReportedAsync(
+                vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
     }
 
     private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
@@ -1055,17 +1051,15 @@
     }
 
     @GuardedBy("mLock")
-    private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
+    private void reportFinishedVibrationLocked() {
         Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+        mCurrentVibration.unlinkToDeath();
         HalVibration vib = mCurrentVibration.getVibration();
         if (DEBUG) {
-            Slog.d(TAG, "Reporting vibration " + vib.id + " finished with "
-                    + vibrationEndInfo);
+            Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vib.getStatus());
         }
-        // DO NOT write metrics at this point, wait for the VibrationThread to report the
-        // vibration was released, after all cleanup. The metrics will be reported then.
-        endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false);
         finishAppOpModeLocked(vib.callerInfo);
+        reportEndedVibrationLocked(vib);
     }
 
     private void onSyncedVibrationComplete(long vibrationId) {
@@ -1575,7 +1569,7 @@
 
         @Override
         public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
             try {
                 if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
                     // This sync step requires capabilities this device doesn't have, skipping
@@ -1584,33 +1578,33 @@
                 }
                 return mNativeWrapper.prepareSynced(vibratorIds);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public boolean triggerSyncedVibration(long vibrationId) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
             try {
                 return mNativeWrapper.triggerSynced(vibrationId);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void cancelSyncedVibration() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
             try {
                 mNativeWrapper.cancelSynced();
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void noteVibratorOn(int uid, long duration) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOn");
             try {
                 if (duration <= 0) {
                     // Tried to turn vibrator ON and got:
@@ -1629,38 +1623,20 @@
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error logging VibratorStateChanged to ON", e);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void noteVibratorOff(int uid) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOff");
             try {
                 mBatteryStatsService.noteVibratorOff(uid);
                 mFrameworkStatsLogger.writeVibratorStateOffAsync(uid);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        @Override
-        public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) {
-            if (DEBUG) {
-                Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo);
-            }
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationCompleted");
-            try {
-                synchronized (mLock) {
-                    if (mCurrentVibration != null
-                            && mCurrentVibration.getVibration().id == vibrationId) {
-                        reportFinishedVibrationLocked(vibrationEndInfo);
-                    }
-                }
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
@@ -1669,7 +1645,8 @@
             if (DEBUG) {
                 Slog.d(TAG, "VibrationThread released after finished vibration");
             }
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId);
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
+
             try {
                 synchronized (mLock) {
                     if (DEBUG) {
@@ -1682,11 +1659,8 @@
                                 mCurrentVibration.getVibration().id, vibrationId));
                     }
                     if (mCurrentVibration != null) {
-                        // This is when we consider the current vibration complete, so report
-                        // metrics.
-                        mFrameworkStatsLogger.writeVibrationReportedAsync(
-                                mCurrentVibration.getVibration().getStatsInfo(
-                                        /* completionUptimeMillis= */ SystemClock.uptimeMillis()));
+                        // This is when we consider the current vibration complete, report metrics.
+                        reportFinishedVibrationLocked();
                         mCurrentVibration = null;
                     }
                     if (mNextVibration != null) {
@@ -1696,13 +1670,12 @@
                                 nextConductor);
                         if (vibrationEndInfo != null) {
                             // Failed to start the vibration, end it and report metrics right away.
-                            endVibrationLocked(nextConductor.getVibration(),
-                                    vibrationEndInfo, /* shouldWriteStats= */ true);
+                            endVibrationLocked(nextConductor.getVibration(), vibrationEndInfo);
                         }
                     }
                 }
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
     }
@@ -1929,8 +1902,7 @@
                         + " with end info: " + vibrationEndInfo);
             }
             // Clearing next vibration before playing it, end it and report metrics right away.
-            endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo,
-                    /* shouldWriteStats= */ true);
+            endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo);
             mNextVibration = null;
         }
     }
@@ -1938,23 +1910,24 @@
     /**
      * Ends the external vibration, and clears related service state.
      *
-     * @param vibrationEndInfo        the status and related info to end the associated Vibration
+     * @param status                  the status to end the associated Vibration
+     * @param endedBy                 the caller that caused this vibration to end
      * @param continueExternalControl indicates whether external control will continue. If not, the
      *                                HAL will have external control turned off.
      */
     @GuardedBy("mLock")
-    private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo,
+    private void endExternalVibrateLocked(Status status, CallerInfo endedBy,
             boolean continueExternalControl) {
         if (mCurrentExternalVibration == null) {
             return;
         }
+        mCurrentExternalVibration.requestEnd(status, endedBy, /* immediate= */ true);
         mCurrentExternalVibration.unlinkToDeath();
         if (!continueExternalControl) {
             setExternalControl(false, mCurrentExternalVibration.stats);
         }
         // The external control was turned off, end it and report metrics right away.
-        endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo,
-                /* shouldWriteStats= */ true);
+        reportEndedVibrationLocked(mCurrentExternalVibration);
         mCurrentExternalVibration = null;
     }
 
@@ -2010,7 +1983,7 @@
 
         @Override
         public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
             try {
                 // Create Vibration.Stats as close to the received request as possible, for
                 // tracking.
@@ -2022,9 +1995,7 @@
 
                 synchronized (mLock) {
                     if (!hasExternalControlCapability()) {
-                        endVibrationLocked(externalVibration,
-                                new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED),
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, Status.IGNORED_UNSUPPORTED);
                         return externalVibration.getScale();
                     }
 
@@ -2035,9 +2006,7 @@
                         Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
                                 + " tried to play externally controlled vibration"
                                 + " without VIBRATE permission, ignoring.");
-                        endVibrationLocked(externalVibration,
-                                new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION),
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, Status.IGNORED_MISSING_PERMISSION);
                         return externalVibration.getScale();
                     }
 
@@ -2058,8 +2027,7 @@
                     }
 
                     if (vibrationEndInfo != null) {
-                        endVibrationLocked(externalVibration, vibrationEndInfo,
-                                /* shouldWriteStats= */ true);
+                        endVibrationLocked(externalVibration, vibrationEndInfo);
                         return externalVibration.getScale();
                     }
 
@@ -2092,15 +2060,32 @@
                         // as we would need to mute the old one still if it came from a different
                         // controller.
                         alreadyUnderExternalControl = true;
-                        mCurrentExternalVibration.notifyEnded();
                         externalVibration.stats.reportInterruptedAnotherVibration(
-                                mCurrentExternalVibration.callerInfo);
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
-                                        externalVibration.callerInfo),
-                                /* continueExternalControl= */ true);
+                                mCurrentExternalVibration.getCallerInfo());
+                        endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED,
+                                externalVibration.callerInfo, /* continueExternalControl= */ true);
                     }
-
+                }
+                // Wait for lock and interact with HAL to set external control outside main lock.
+                if (waitForCompletion) {
+                    if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
+                        Slog.e(TAG, "Timed out waiting for vibration to cancel");
+                        synchronized (mLock) {
+                            endVibrationLocked(externalVibration, Status.IGNORED_ERROR_CANCELLING);
+                            return externalVibration.getScale();
+                        }
+                    }
+                }
+                if (!alreadyUnderExternalControl) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Vibrator going under external control.");
+                    }
+                    setExternalControl(true, externalVibration.stats);
+                }
+                synchronized (mLock) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Playing external vibration: " + vib);
+                    }
                     VibrationAttributes attrs = fixupVibrationAttributes(
                             vib.getVibrationAttributes(),
                             /* effect= */ null);
@@ -2113,43 +2098,20 @@
                     mCurrentExternalVibration = externalVibration;
                     externalVibration.linkToDeath(this::onExternalVibrationBinderDied);
                     externalVibration.scale(mVibrationScaler, attrs.getUsage());
-                }
 
-                if (waitForCompletion) {
-                    if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
-                        Slog.e(TAG, "Timed out waiting for vibration to cancel");
-                        synchronized (mLock) {
-                            // Trigger endExternalVibrateLocked to unlink to death recipient.
-                            endExternalVibrateLocked(
-                                    new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING),
-                                    /* continueExternalControl= */ false);
-                            // Mute the request, vibration will be ignored.
-                            externalVibration.muteScale();
-                        }
-                        return externalVibration.getScale();
-                    }
+                    // Vibrator will start receiving data from external channels after this point.
+                    // Report current time as the vibration start time, for debugging.
+                    externalVibration.stats.reportStarted();
+                    return externalVibration.getScale();
                 }
-                if (!alreadyUnderExternalControl) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Vibrator going under external control.");
-                    }
-                    setExternalControl(true, externalVibration.stats);
-                }
-                if (DEBUG) {
-                    Slog.d(TAG, "Playing external vibration: " + vib);
-                }
-                // Vibrator will start receiving data from external channels after this point.
-                // Report current time as the vibration start time, for debugging.
-                externalVibration.stats.reportStarted();
-                return externalVibration.getScale();
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void onExternalVibrationStop(ExternalVibration vib) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
             try {
                 synchronized (mLock) {
                     if (mCurrentExternalVibration != null
@@ -2157,13 +2119,12 @@
                         if (DEBUG) {
                             Slog.d(TAG, "Stopping external vibration: " + vib);
                         }
-                        endExternalVibrateLocked(
-                                new Vibration.EndInfo(Status.FINISHED),
+                        endExternalVibrateLocked(Status.FINISHED, /* endedBy= */ null,
                                 /* continueExternalControl= */ false);
                     }
                 }
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
@@ -2182,8 +2143,7 @@
                     if (DEBUG) {
                         Slog.d(TAG, "External vibration finished because binder died");
                     }
-                    endExternalVibrateLocked(
-                            new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED),
+                    endExternalVibrateLocked(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null,
                             /* continueExternalControl= */ false);
                 }
             }
@@ -2232,32 +2192,39 @@
 
         @Override
         public int onCommand(String cmd) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onCommand " + cmd);
             try {
                 if ("list".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: list");
                     return runListVibrators();
                 }
                 if ("synced".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: synced");
                     return runMono();
                 }
                 if ("combined".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: combined");
                     return runStereo();
                 }
                 if ("sequential".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: sequential");
                     return runSequential();
                 }
                 if ("xml".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: xml");
                     return runXml();
                 }
                 if ("cancel".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: cancel");
                     return runCancel();
                 }
                 if ("feedback".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: feedback");
                     return runHapticFeedback();
                 }
+                Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: default");
                 return handleDefaultCommands(cmd);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 4754ffb..946b61a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1468,7 +1468,7 @@
                         || change == PACKAGE_TEMPORARY_CHANGE) {
                     changed = true;
                     if (doit) {
-                        Slog.w(TAG, "Wallpaper uninstalled, removing: "
+                        Slog.e(TAG, "Wallpaper uninstalled, removing: "
                                 + wallpaper.getComponent());
                         clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                     }
@@ -1491,7 +1491,7 @@
                             PackageManager.MATCH_DIRECT_BOOT_AWARE
                                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
                 } catch (NameNotFoundException e) {
-                    Slog.w(TAG, "Wallpaper component gone, removing: "
+                    Slog.e(TAG, "Wallpaper component gone, removing: "
                             + wallpaper.getComponent());
                     clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                 }
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 6740153..ab5316f 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -41,7 +41,8 @@
 
 import com.android.internal.util.XmlUtils;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -318,8 +319,9 @@
             if (webviewPinQuota <= 0) {
                 break;
             }
-            int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP);
-            webviewPinQuota -= bytesPinned;
+            PinnedFile pf = pinnerService.pinFile(
+                    apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true);
+            webviewPinQuota -= pf.bytesPinned;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d29ff54..4092a0b 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -100,6 +100,7 @@
 import android.app.WaitResult;
 import android.app.WindowConfiguration;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -182,7 +183,7 @@
      * Feature flag for go/activity-security rules
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Disabled
     static final long ASM_RESTRICTIONS = 230590090L;
 
     private final ActivityTaskManagerService mService;
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index 241390c1..fbf9478 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -170,7 +170,7 @@
      * </ul>
      */
     boolean shouldApplyFreeformTreatmentForCameraCompat() {
-        return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord,
+        return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord,
                 OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 67bfd76..5338c01 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -48,8 +48,9 @@
         // without the need to restart the device.
         final boolean needsDisplayRotationCompatPolicy =
                 wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
-        final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
-                && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
+        final boolean needsCameraCompatFreeformPolicy =
+                Flags.enableCameraCompatForDesktopWindowing()
+                        && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
         if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
             mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
             mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2259b5a..515f148 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -1120,7 +1120,9 @@
             @Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
             int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) {
         // BAL Exception allowed in all cases
-        if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
+        if (balCode == BAL_ALLOW_ALLOWLISTED_UID
+                || (android.security.Flags.asmReintroduceGracePeriod()
+                    && balCode == BAL_ALLOW_GRACE_PERIOD)) {
             return true;
         }
 
@@ -1173,10 +1175,15 @@
                 ArrayList<Task> visibleTasks = displayArea.getVisibleTasks();
                 for (int i = 0; i < visibleTasks.size(); i++) {
                     Task task = visibleTasks.get(i);
-                    if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
-                        bas.optedIn(task.getTopMostActivity());
-                    } else {
+                    if (android.security.Flags.asmReintroduceGracePeriod()) {
                         bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas);
+                    } else {
+                        if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
+                            bas.optedIn(task.getTopMostActivity());
+                        } else {
+                            bas = checkTopActivityForAsm(
+                                task, callingUid, /*sourceRecord*/null, bas);
+                        }
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index e3232e0..d6caa1a 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -124,7 +124,7 @@
      */
     @VisibleForTesting
     boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
-        return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled(
+        return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
                 ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 0fa1a21..e6f6215 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2131,7 +2131,8 @@
         final DecorInsets.Info newInfo = mDecorInsets.mTmpInfo;
         final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh);
         final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh);
-        if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)
+        final boolean sameConfigFrame = newInfo.mConfigFrame.equals(currentInfo.mConfigFrame);
+        if (sameConfigFrame
                 && newInfo.mOverrideConfigFrame.equals(currentInfo.mOverrideConfigFrame)) {
             // Even if the config frame is not changed in current rotation, it may change the
             // insets in other rotations if the frame of insets source is changed.
@@ -2155,7 +2156,7 @@
         }
         mDecorInsets.invalidate();
         mDecorInsets.mInfoForRotation[rotation].set(newInfo);
-        return true;
+        return !sameConfigFrame;
     }
 
     DecorInsets.Info getDecorInsetsInfo(int rotation, int w, int h) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 232c3b6..dcf0319 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -188,9 +188,8 @@
      * the application did not handle.
      */
     @Override
-    public KeyEvent dispatchUnhandledKey(
-            IBinder focusedToken, KeyEvent event, int policyFlags) {
-        return mService.mPolicy.dispatchUnhandledKey(focusedToken, event, policyFlags);
+    public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+        return mService.mPolicy.interceptUnhandledKey(event, focusedToken);
     }
 
     /** Callback to get pointer layer. */
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index ddbfd70..d7dc459 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -222,7 +222,8 @@
             UserHandle clientUser) {
         final InputConsumerImpl existingConsumer = getInputConsumer(name);
         if (existingConsumer != null && existingConsumer.mClientUser.equals(clientUser)) {
-            throw new IllegalStateException("Existing input consumer found with name: " + name
+            destroyInputConsumer(existingConsumer.mToken);
+            Slog.w(TAG_WM, "Replacing existing input consumer found with name: " + name
                     + ", display: " + mDisplayId + ", user: " + clientUser);
         }
 
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9da848a..bf623b2 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -24,6 +24,8 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -2040,10 +2042,15 @@
         final boolean isOtherUndefinedMode = otherWindowingMode == WINDOWING_MODE_UNDEFINED;
 
         // An activity type and windowing mode is compatible if they are the exact same type/mode,
-        // or if one of the type/modes is undefined
+        // or if one of the type/modes is undefined. This is with the exception of
+        // freeform/fullscreen where both modes are assumed to be compatible with each other.
         final boolean isCompatibleType = activityType == otherActivityType
                 || isUndefinedType || isOtherUndefinedType;
         final boolean isCompatibleMode = windowingMode == otherWindowingMode
+                || (windowingMode == WINDOWING_MODE_FREEFORM
+                && otherWindowingMode == WINDOWING_MODE_FULLSCREEN)
+                || (windowingMode == WINDOWING_MODE_FULLSCREEN
+                && otherWindowingMode == WINDOWING_MODE_FREEFORM)
                 || isUndefinedMode || isOtherUndefinedMode;
 
         return isCompatibleType && isCompatibleMode;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 86bb75a..14f034b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -66,6 +66,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.PAUSING;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -6177,6 +6178,8 @@
 
     void maybeApplyLastRecentsAnimationTransaction() {
         if (mLastRecentsAnimationTransaction != null) {
+            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+                    "Applying last recents animation transaction.");
             final SurfaceControl.Transaction tx = getPendingTransaction();
             if (mLastRecentsAnimationOverlay != null) {
                 tx.reparent(mLastRecentsAnimationOverlay, mSurfaceControl);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b8f47cc..9426347 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9007,7 +9007,9 @@
 
         final boolean isInputTargetNotFocused =
                 mFocusedInputTarget != t && mFocusedInputTarget != null;
-        if (!isInputTargetNotFocused) {
+        final boolean isTouchOnFocusedDisplay = mFocusedInputTarget != null
+                && t.getDisplayId() == mFocusedInputTarget.getDisplayId();
+        if (!(isInputTargetNotFocused && isTouchOnFocusedDisplay)) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 476443a..f35f2b3 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -799,7 +799,12 @@
             }
         } finally {
             if (deferTransitionReady) {
-                chain.mTransition.continueTransitionReady();
+                if (chain.mTransition.isCollecting()) {
+                    chain.mTransition.continueTransitionReady();
+                } else {
+                    Slog.wtf(TAG, "Too late, transition : " + chain.mTransition.getSyncId()
+                            + " state: " + chain.mTransition.getState() + " is not collecting");
+                }
             }
             mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
             if (deferResume) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4c4b4f6..0e3ab63 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1969,6 +1969,13 @@
     boolean isReadyForDisplay() {
         final boolean parentAndClientVisible = !isParentWindowHidden()
                 && mViewVisibility == View.VISIBLE;
+        // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to
+        // shell transitions
+        if (!mTransitionController.isShellTransitionsEnabled()) {
+            return mHasSurface && isVisibleByPolicy() && !mDestroying
+                    && ((parentAndClientVisible && mToken.isVisible())
+                    || isAnimating(TRANSITION | PARENTS));
+        }
         return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible()
                 && (parentAndClientVisible || isAnimating(TRANSITION | PARENTS));
     }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 5cd117b..efca902 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -56,6 +56,7 @@
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <server_configurable_flags/get_flags.h>
+#include <ui/LogicalDisplayId.h>
 #include <ui/Region.h>
 #include <utils/Log.h>
 #include <utils/Looper.h>
@@ -64,6 +65,7 @@
 
 #include <atomic>
 #include <cinttypes>
+#include <map>
 #include <vector>
 
 #include "android_hardware_display_DisplayViewport.h"
@@ -343,7 +345,7 @@
     void setTouchpadRightClickZoneEnabled(bool enabled);
     void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
     void setShowTouches(bool enabled);
-    void setInteractive(bool interactive);
+    void setNonInteractiveDisplays(const std::set<ui::LogicalDisplayId>& displayIds);
     void reloadCalibration();
     void reloadPointerIcons();
     void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
@@ -508,9 +510,11 @@
 
         // Keycodes to be remapped.
         std::map<int32_t /* fromKeyCode */, int32_t /* toKeyCode */> keyRemapping{};
+
+        // Displays which are non-interactive.
+        std::set<ui::LogicalDisplayId> nonInteractiveDisplays;
     } mLocked GUARDED_BY(mLock);
 
-    std::atomic<bool> mInteractive;
     void updateInactivityTimeoutLocked();
     void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
     void ensureSpriteControllerLocked();
@@ -524,12 +528,13 @@
     void forEachPointerControllerLocked(std::function<void(PointerController&)> apply)
             REQUIRES(mLock);
     PointerIcon loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId, PointerIconStyle type);
+    bool isDisplayInteractive(ui::LogicalDisplayId displayId);
 
     static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
 };
 
 NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& looper)
-      : mLooper(looper), mInteractive(true) {
+      : mLooper(looper) {
     JNIEnv* env = jniEnv();
 
     mServiceObj = env->NewGlobalRef(serviceObj);
@@ -547,9 +552,13 @@
 
 void NativeInputManager::dump(std::string& dump) {
     dump += "Input Manager State:\n";
-    dump += StringPrintf(INDENT "Interactive: %s\n", toString(mInteractive.load()));
     { // acquire lock
         std::scoped_lock _l(mLock);
+        auto logicalDisplayIdToString = [](const ui::LogicalDisplayId& displayId) {
+            return std::to_string(displayId.val());
+        };
+        dump += StringPrintf(INDENT "Display not interactive: %s\n",
+                             dumpSet(mLocked.nonInteractiveDisplays, streamableToString).c_str());
         dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
                              toString(mLocked.systemUiLightsOut));
         dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
@@ -1476,8 +1485,10 @@
     mInputManager->getDispatcher().requestPointerCapture(windowToken, enabled);
 }
 
-void NativeInputManager::setInteractive(bool interactive) {
-    mInteractive = interactive;
+void NativeInputManager::setNonInteractiveDisplays(
+        const std::set<ui::LogicalDisplayId>& displayIds) {
+    std::scoped_lock _l(mLock);
+    mLocked.nonInteractiveDisplays = displayIds;
 }
 
 void NativeInputManager::reloadCalibration() {
@@ -1606,7 +1617,7 @@
     // - Ignore untrusted events and pass them along.
     // - Ask the window manager what to do with normal events and trusted injected events.
     // - For normal events wake and brighten the screen if currently off or dim.
-    const bool interactive = mInteractive.load();
+    const bool interactive = isDisplayInteractive(keyEvent.getDisplayId());
     if (interactive) {
         policyFlags |= POLICY_FLAG_INTERACTIVE;
     }
@@ -1644,7 +1655,7 @@
     // - No special filtering for injected events required at this time.
     // - Filter normal events based on screen state.
     // - For normal events brighten (but do not wake) the screen if currently dim.
-    const bool interactive = mInteractive.load();
+    const bool interactive = isDisplayInteractive(displayId);
     if (interactive) {
         policyFlags |= POLICY_FLAG_INTERACTIVE;
     }
@@ -1683,6 +1694,24 @@
     }
 }
 
+bool NativeInputManager::isDisplayInteractive(ui::LogicalDisplayId displayId) {
+    // If an input event doesn't have an associated id, use the default display id
+    if (displayId == ui::LogicalDisplayId::INVALID) {
+        displayId = ui::LogicalDisplayId::DEFAULT;
+    }
+
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        auto it = mLocked.nonInteractiveDisplays.find(displayId);
+        if (it != mLocked.nonInteractiveDisplays.end()) {
+            return false;
+        }
+    } // release lock
+
+    return true;
+}
+
 nsecs_t NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token,
                                                           const KeyEvent& keyEvent,
                                                           uint32_t policyFlags) {
@@ -2372,10 +2401,17 @@
     im->setShowTouches(enabled);
 }
 
-static void nativeSetInteractive(JNIEnv* env, jobject nativeImplObj, jboolean interactive) {
+static void nativeSetNonInteractiveDisplays(JNIEnv* env, jobject nativeImplObj,
+                                            jintArray displayIds) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setInteractive(interactive);
+    const std::vector displayIdsVec = getIntArray(env, displayIds);
+    std::set<ui::LogicalDisplayId> logicalDisplayIds;
+    for (int displayId : displayIdsVec) {
+        logicalDisplayIds.emplace(ui::LogicalDisplayId{displayId});
+    }
+
+    im->setNonInteractiveDisplays(logicalDisplayIds);
 }
 
 static void nativeReloadCalibration(JNIEnv* env, jobject nativeImplObj) {
@@ -3021,7 +3057,7 @@
          (void*)nativeSetShouldNotifyTouchpadHardwareState},
         {"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
         {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
-        {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
+        {"setNonInteractiveDisplays", "([I)V", (void*)nativeSetNonInteractiveDisplays},
         {"reloadCalibration", "()V", (void*)nativeReloadCalibration},
         {"vibrate", "(I[J[III)V", (void*)nativeVibrate},
         {"vibrateCombined", "(I[JLandroid/util/SparseArray;II)V", (void*)nativeVibrateCombined},
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 0eafb59..a07facf 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -512,8 +512,6 @@
                 <xs:annotation name="final"/>
             </xs:element>
         </xs:sequence>
-        <!-- valid value of interpolation if specified: linear -->
-        <xs:attribute name="interpolation" type="xs:string" use="optional"/>
     </xs:complexType>
 
     <xs:complexType name="brightnessPoint">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 355b0ab..5309263 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -91,8 +91,6 @@
   public class ComprehensiveBrightnessMap {
     ctor public ComprehensiveBrightnessMap();
     method @NonNull public final java.util.List<com.android.server.display.config.BrightnessPoint> getBrightnessPoint();
-    method public String getInterpolation();
-    method public void setInterpolation(String);
   }
 
   public class Density {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 407a5a6..4e89b85 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -4152,8 +4152,10 @@
     }
 
     private void checkAllUsersAreAffiliatedWithDevice() {
-        Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(),
-                "operation not allowed when device has unaffiliated users");
+        synchronized (getLockObject()) {
+            Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(),
+                    "operation not allowed when device has unaffiliated users");
+        }
     }
 
     @Override
@@ -11362,7 +11364,7 @@
         if (mOwners.hasDeviceOwner()) {
             return false;
         }
-        
+
         final ComponentName profileOwner = getProfileOwnerAsUser(userId);
         if (profileOwner == null) {
             return false;
@@ -11371,7 +11373,7 @@
         if (isManagedProfile(userId)) {
             return false;
         }
-        
+
         return true;
     }
     private void enforceCanQueryLockTaskLocked(ComponentName who, String callerPackageName) {
@@ -18213,6 +18215,7 @@
         return false;
     }
 
+    @GuardedBy("getLockObject()")
     private boolean areAllUsersAffiliatedWithDeviceLocked() {
         return mInjector.binderWithCleanCallingIdentity(() -> {
             final List<UserInfo> userInfos = mUserManager.getAliveUsers();
@@ -18310,10 +18313,12 @@
 
         final CallerIdentity caller = getCallerIdentity(admin, packageName);
         if (isPermissionCheckFlagEnabled()) {
-            Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
-                    || areAllUsersAffiliatedWithDeviceLocked());
-            enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(),
-                    UserHandle.USER_ALL);
+            synchronized (getLockObject()) {
+                Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
+                        || areAllUsersAffiliatedWithDeviceLocked());
+                enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(),
+                        UserHandle.USER_ALL);
+            }
         } else {
             if (admin != null) {
                 Preconditions.checkCallAuthorization(
@@ -18325,8 +18330,10 @@
                         isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING));
             }
 
-            Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
-                    || areAllUsersAffiliatedWithDeviceLocked());
+            synchronized (getLockObject()) {
+                Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
+                        || areAllUsersAffiliatedWithDeviceLocked());
+            }
         }
 
         DevicePolicyEventLogger
@@ -19384,11 +19391,13 @@
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
                     userId);
-            // TODO(b/369152176): Address difference in behavior regarding addEscrowToken when
-            //  compared with the else branch.
             long tokenHandle = addEscrowToken(
                     token, currentTokenHandle == null ? 0 : currentTokenHandle, userId);
             if (tokenHandle == 0) {
+                mDevicePolicyEngine.removeLocalPolicy(
+                        PolicyDefinition.RESET_PASSWORD_TOKEN,
+                        enforcingAdmin,
+                        userId);
                 return false;
             }
             mDevicePolicyEngine.setLocalPolicy(
@@ -24538,7 +24547,8 @@
             }
         });
     }
-    
+
+    @GuardedBy("getLockObject()")
     private void migrateUserControlDisabledPackagesLocked() {
         Binder.withCleanCallingIdentity(() -> {
             List<UserInfo> users = mUserManager.getUsers();
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3dbcf21..ce6f1ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -103,6 +103,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.policy.AttributeCache;
@@ -204,6 +205,7 @@
 import com.android.server.pdb.PersistentDataBlockService;
 import com.android.server.people.PeopleService;
 import com.android.server.permission.access.AccessCheckingService;
+import com.android.server.pinner.PinnerService;
 import com.android.server.pm.ApexManager;
 import com.android.server.pm.ApexSystemServiceInfo;
 import com.android.server.pm.BackgroundInstallControlService;
@@ -940,6 +942,12 @@
         // Setup the default WTF handler
         RuntimeInit.setDefaultApplicationWtfHandler(SystemServer::handleEarlySystemWtf);
 
+        // Initialize the application shared memory region.
+        // This needs to happen before any system services are started,
+        // as they may rely on the shared memory region having been initialized.
+        ApplicationSharedMemory instance = ApplicationSharedMemory.create();
+        ApplicationSharedMemory.setInstance(instance);
+
         // Start services.
         try {
             t.traceBegin("StartServices");
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
new file mode 100644
index 0000000..fead05b
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -0,0 +1,46 @@
+/*
+ * 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.server.supervision;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+
+/**
+ * Local system service interface for {@link SupervisionService}.
+ *
+ * @hide Only for use within Android OS.
+ */
+public abstract class SupervisionManagerInternal {
+    /**
+     * Returns whether supervision is enabled for the specified user
+     *
+     * @param userId The user to retrieve the supervision state for
+     * @return whether the user is supervised
+     */
+    public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
+
+    /**
+     * Sets whether the supervision lock screen should be shown for the specified user
+     *
+     * @param userId The user set the superivision state for
+     * @param enabled Whether or not the superivision lock screen needs to be shown
+     * @param options Optional configuration parameters for the supervision lock screen
+     */
+    public abstract void setSupervisionLockscreenEnabledForUser(
+            @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 7ffd0ec..4c515c1 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -18,14 +18,22 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.supervision.ISupervisionManager;
 import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
+import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -38,13 +46,25 @@
 
     private final Context mContext;
 
+    // TODO(b/362756788): Does this need to be a LockGuard lock?
+    private final Object mLockDoNoUseDirectly = new Object();
+
+    @GuardedBy("getLockObject()")
+    private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+
+    private final UserManagerInternal mUserManagerInternal;
+
     public SupervisionService(Context context) {
-        mContext = context.createAttributionContext("SupervisionService");
+        mContext = context.createAttributionContext(LOG_TAG);
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+        mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
     }
 
     @Override
-    public boolean isSupervisionEnabled() {
-        return false;
+    public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+        synchronized (getLockObject()) {
+            return getUserDataLocked(userId).supervisionEnabled;
+        }
     }
 
     @Override
@@ -60,11 +80,44 @@
     }
 
     @Override
-    protected void dump(
-            @NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) {
-        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
+    protected void dump(@NonNull FileDescriptor fd,
+            @NonNull PrintWriter printWriter, @Nullable String[] args) {
+        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
 
-        fout.println("Supervision enabled: " + isSupervisionEnabled());
+        try (var pw = new IndentingPrintWriter(printWriter, "  ")) {
+            pw.println("SupervisionService state:");
+            pw.increaseIndent();
+
+            var users = mUserManagerInternal.getUsers(false);
+            synchronized (getLockObject()) {
+                for (var user : users) {
+                    getUserDataLocked(user.id).dump(pw);
+                    pw.println();
+                }
+            }
+        }
+    }
+
+    private Object getLockObject() {
+        return mLockDoNoUseDirectly;
+    }
+
+    @NonNull
+    @GuardedBy("getLockObject()")
+    SupervisionUserData getUserDataLocked(@UserIdInt int userId) {
+        SupervisionUserData data = mUserData.get(userId);
+        if (data == null) {
+            // TODO(b/362790738): Do not create user data for nonexistent users.
+            data = new SupervisionUserData(userId);
+            mUserData.append(userId, data);
+        }
+        return data;
+    }
+
+    void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+        synchronized (getLockObject()) {
+            getUserDataLocked(userId).supervisionEnabled = enabled;
+        }
     }
 
     public static class Lifecycle extends SystemService {
@@ -77,7 +130,35 @@
 
         @Override
         public void onStart() {
+            publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
             publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
         }
     }
+
+    final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+        public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+            synchronized (getLockObject()) {
+                return getUserDataLocked(userId).supervisionEnabled;
+            }
+        }
+
+        @Override
+        public void setSupervisionLockscreenEnabledForUser(
+                @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+            synchronized (getLockObject()) {
+                SupervisionUserData data = getUserDataLocked(userId);
+                data.supervisionLockScreenEnabled = enabled;
+                data.supervisionLockScreenOptions = options;
+            }
+        }
+    };
+
+    private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+        @Override
+        public void onUserRemoved(UserInfo user) {
+            synchronized (getLockObject()) {
+                mUserData.remove(user.id);
+            }
+        }
+    }
 }
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
index 3aba24a..2adaae3 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
@@ -17,8 +17,7 @@
 package com.android.server.supervision;
 
 import android.os.ShellCommand;
-
-import java.io.PrintWriter;
+import android.os.UserHandle;
 
 public class SupervisionServiceShellCommand extends ShellCommand {
     private final SupervisionService mService;
@@ -32,30 +31,29 @@
         if (cmd == null) {
             return handleDefaultCommands(null);
         }
-        final PrintWriter pw = getOutPrintWriter();
         switch (cmd) {
-            case "help": return help(pw);
-            case "is-enabled": return isEnabled(pw);
+            case "enable": return setEnabled(true);
+            case "disable": return setEnabled(false);
             default: return handleDefaultCommands(cmd);
         }
     }
 
-    private int help(PrintWriter pw) {
-        pw.println("Supervision service commands:");
-        pw.println("  help");
-        pw.println("      Prints this help text");
-        pw.println("  is-enabled");
-        pw.println("      Is supervision enabled");
-        return 0;
-    }
-
-    private int isEnabled(PrintWriter pw) {
-        pw.println(mService.isSupervisionEnabled());
+    private int setEnabled(boolean enabled) {
+        final var pw = getOutPrintWriter();
+        final var userId = UserHandle.parseUserArg(getNextArgRequired());
+        mService.setSupervisionEnabledForUser(userId, enabled);
         return 0;
     }
 
     @Override
     public void onHelp() {
-        help(getOutPrintWriter());
+        final var pw = getOutPrintWriter();
+        pw.println("Supervision service (supervision) commands:");
+        pw.println("  help");
+        pw.println("      Prints this help text");
+        pw.println("  enable <USER_ID>");
+        pw.println("      Enables supervision for the given user.");
+        pw.println("  disable <USER_ID>");
+        pw.println("      Disables supervision for the given user.");
     }
 }
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
new file mode 100644
index 0000000..5616237
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -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.server.supervision;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+import android.util.IndentingPrintWriter;
+
+/** User specific data, used internally by the {@link SupervisionService}. */
+public class SupervisionUserData {
+    public final @UserIdInt int userId;
+    public boolean supervisionEnabled;
+    public boolean supervisionLockScreenEnabled;
+    @Nullable public Bundle supervisionLockScreenOptions;
+
+    public SupervisionUserData(@UserIdInt int userId) {
+        this.userId = userId;
+    }
+
+    void dump(@NonNull IndentingPrintWriter pw) {
+        pw.println();
+        pw.println("User " + userId + ":");
+        pw.increaseIndent();
+        pw.println("supervisionEnabled: " + supervisionEnabled);
+        pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled);
+        pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions);
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index e5d3153..72cbac3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -35,6 +35,7 @@
 public final class InputMethodManagerServiceTests {
     static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2;
     static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3;
+    private static final int TEST_IME_USER_ID = 1;
 
     static InputMethodManagerService.ImeDisplayValidator sChecker =
             (displayId) -> {
@@ -102,7 +103,8 @@
                 null,
                 null,
                 null,
-                null));
+                null,
+                TEST_IME_USER_ID));
 
         history.dump(new PrintWriter(writer), "" /* prefix */);
 
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
                     android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
                 PackageManager.PERMISSION_GRANTED
             }
+            whenever(this.checkPermission(
+                eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+                PackageManager.PERMISSION_GRANTED
+            }
         }
         val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
             whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
index b21c349..2144785 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
@@ -94,6 +94,7 @@
             ParsedService::getIntents,
             ParsedService::getProperties,
             Intent::getCategories,
+            Intent::getExtraIntentKeys,
             PackageUserState::getDisabledComponents,
             PackageUserState::getEnabledComponents,
             PackageUserState::getSharedLibraryOverlayPaths,
diff --git a/services/tests/RemoteProvisioningServiceTests/Android.bp b/services/tests/RemoteProvisioningServiceTests/Android.bp
index 19c9136..3a73c39 100644
--- a/services/tests/RemoteProvisioningServiceTests/Android.bp
+++ b/services/tests/RemoteProvisioningServiceTests/Android.bp
@@ -31,7 +31,6 @@
         "service-rkp.impl",
         "services.core",
         "truth",
-        "truth-java8-extension",
     ],
     test_suites: [
         "device-tests",
diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
index 007c0db..a1616c6 100644
--- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
+++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
@@ -17,7 +17,6 @@
 package com.android.server.security.rkp;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
diff --git a/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt
new file mode 100644
index 0000000..413eb31
--- /dev/null
+++ b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appfunctions
+
+import android.app.appsearch.GenericDocument
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+
+@RunWith(JUnit4::class)
+class GenericDocumentWrapperTest {
+
+    @Test
+    fun parcelUnparcel() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+
+        val recovered = parcelUnparcel(wrapper)
+
+        assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+    @Test
+    fun parcelUnparcel_afterGetValue() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+        assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42)
+
+        val recovered = parcelUnparcel(wrapper)
+
+        assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+
+    @Test
+    fun getValue() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+
+        assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+    private fun parcelUnparcel(obj: GenericDocumentWrapper): GenericDocumentWrapper {
+        val parcel = Parcel.obtain()
+        try {
+            obj.writeToParcel(parcel, 0)
+            parcel.setDataPosition(0)
+            return GenericDocumentWrapper.CREATOR.createFromParcel(parcel)
+        } finally {
+            parcel.recycle()
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index fd05b26..8e1be9a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -392,7 +392,7 @@
     public void testInvalidLuxThrottling() throws Exception {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getInvalidLuxThrottling(), getValidProxSensor(),
-                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
 
         Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
                 mDisplayDeviceConfig.getLuxThrottlingData();
@@ -600,7 +600,7 @@
     public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
-                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
         assertNull(mDisplayDeviceConfig.getProximitySensor());
     }
 
@@ -608,7 +608,7 @@
     public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
-                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
         assertEquals("test_proximity_sensor",
                 mDisplayDeviceConfig.getProximitySensor().type);
         assertEquals("Test Proximity Sensor",
@@ -803,7 +803,7 @@
     @Test
     public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
 
         assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -820,14 +820,14 @@
     @Test
     public void testBrightnessCapForWearBedtimeMode() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
         assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
     }
 
     @Test
     public void testAutoBrightnessBrighteningLevels() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
 
         assertArrayEquals(new float[]{0.0f, 80},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -890,7 +890,7 @@
         when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
         setupDisplayDeviceConfigFromConfigResourceFile();
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
 
         assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
                         brightnessIntToFloat(150)},
@@ -929,7 +929,7 @@
         when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
         when(mResources.getBoolean(R.bool.config_evenDimmerEnabled)).thenReturn(true);
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ true));
 
         assertTrue(mDisplayDeviceConfig.isEvenDimmerAvailable());
         assertEquals(0.01f, mDisplayDeviceConfig.getBacklightFromBrightness(0.002f), ZERO_DELTA);
@@ -1365,7 +1365,7 @@
 
     private String getContent() {
         return getContent(getValidLuxThrottling(), getValidProxSensor(),
-                /* includeIdleMode= */ true, false);
+                /* includeIdleMode= */ true, /* enableEvenDimmer= */ false);
     }
 
     private String getContent(String brightnessCapConfig, String proxSensor,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 120cc84..f5bed99 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -29,8 +29,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -45,6 +47,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.util.Spline;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.SurfaceControl;
@@ -59,6 +62,7 @@
 import com.android.internal.R;
 import com.android.server.LocalServices;
 import com.android.server.display.LocalDisplayAdapter.BacklightAdapter;
+import com.android.server.display.color.ColorDisplayService;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.display.notifications.DisplayNotificationManager;
@@ -119,6 +123,8 @@
     private DisplayManagerFlags mFlags;
     @Mock
     private DisplayPowerController mMockedDisplayPowerController;
+    @Mock
+    private ColorDisplayService.ColorDisplayServiceInternal mMockedColorDisplayServiceInternal;
 
     private Handler mHandler;
 
@@ -133,6 +139,11 @@
     private Injector mInjector;
 
     @Mock
+    private DisplayDeviceConfig mMockDisplayDeviceConfig;
+    @Mock
+    private BacklightAdapter mMockBacklightAdapter;
+
+    @Mock
     private LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy;
     private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
     private static final int[] BACKLIGHT_RANGE = { 1, 255 };
@@ -150,6 +161,9 @@
         doReturn(mMockedResources).when(mMockedContext).getResources();
         LocalServices.removeServiceForTest(LightsManager.class);
         LocalServices.addService(LightsManager.class, mMockedLightsManager);
+        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+        LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+                mMockedColorDisplayServiceInternal);
         mInjector = new Injector();
         when(mSurfaceControlProxy.getBootDisplayModeSupport()).thenReturn(true);
         mAdapter = new LocalDisplayAdapter(mMockedSyncRoot, mMockedContext, mHandler,
@@ -211,7 +225,15 @@
         when(mMockedResources.getIntArray(
                 com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
                 .thenReturn(new int[]{});
+
+        when(mMockedColorDisplayServiceInternal.fetchEvenDimmerSpline(3)).thenReturn(
+                new Spline.LinearSpline(
+                        new float[]{2f, 3.0f, 500f, 2000f},
+                        new float[]{100, 0, 0, 0}));
+        when(mMockDisplayDeviceConfig.isEvenDimmerAvailable()).thenReturn(true);
+
         doReturn(true).when(mFlags).isDisplayOffloadEnabled();
+        doReturn(true).when(mFlags).isEvenDimmerEnabled();
         initDisplayOffloadSession();
     }
 
@@ -222,6 +244,122 @@
         }
     }
 
+    @Test
+    public void testEvenDimmer() throws InterruptedException {
+        // Set up
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+        // brightness|backlight| nits | strength
+        //  0.5      |   0.45  | 600  |  0     // initial setup value
+        //  0.4      |   0.35  | 500  |  0     // normal range value
+        //  0.31     |   0.2   |   3  |  0     // transition point
+        //  0.16     |  0.125  | 2.5  |  50    // mid point of even dimmer
+        //  0.1      |   0.05  |   2  |  100   // bottom of even dimmer range
+        //  0.05     |   0.01  |   1  |  100+ // beyond strength=100 range (should still return 100)
+        when(mMockDisplayDeviceConfig.getEvenDimmerTransitionPoint()).thenReturn(0.31f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.5f)).thenReturn(0.45f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.4f)).thenReturn(0.35f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.31f)).thenReturn(0.2f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.16f)).thenReturn(0.125f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.1f)).thenReturn(0.05f);
+        when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.05f)).thenReturn(0.01f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.45f)).thenReturn(600f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.35f)).thenReturn(500f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.2f)).thenReturn(3f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.125f)).thenReturn(2.5f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.05f)).thenReturn(2f);
+        when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.01f)).thenReturn(1f);
+
+        // initialise brightness to 0.5
+        Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON,
+                0.5f, 0.5f, null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        verify(mSurfaceControlProxy).setDisplayPowerMode(any(), anyInt());
+        verify(mMockBacklightAdapter).setBacklight(anyFloat(), anyFloat(), anyFloat(), anyFloat());
+        verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(false), eq(0));
+        verify(mMockedColorDisplayServiceInternal).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up normal brightness range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+                null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify normal brightness range
+        verify(mMockBacklightAdapter).setBacklight(0.35f, 500f, 0.35f, 500f);
+        verify(mMockedColorDisplayServiceInternal,
+                times(1)) // no more, since the strength is the same
+                .applyEvenDimmerColorChanges(eq(false), eq(0));
+        verify(mMockedColorDisplayServiceInternal, times(2)).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up even dimmer edge range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.31f,
+                0.31f, null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify even dimmer edge range
+        verify(mMockBacklightAdapter).setBacklight(0.2f, 3f, 0.2f, 3f);
+        // verify no more times, since the strength and enabled-ness is the same
+        verify(mMockedColorDisplayServiceInternal, times(1)).applyEvenDimmerColorChanges(eq(false),
+                eq(0));
+        verify(mMockedColorDisplayServiceInternal, times(3)).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up mid point of even dimmer range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.16f,
+                0.16f, null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify within even dimmer range
+        verify(mMockBacklightAdapter).setBacklight(0.125f, 2.5f, 0.125f, 2.5f);
+        verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(50));
+        verify(mMockedColorDisplayServiceInternal, times(4)).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up within even dimmer range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.1f, 0.1f,
+                null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify within even dimmer range
+        verify(mMockBacklightAdapter).setBacklight(0.05f, 2f, 0.05f, 2f);
+        verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+        verify(mMockedColorDisplayServiceInternal, times(5)).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up below even dimmer range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.05f,
+                0.05f, null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify within even dimmer range
+        verify(mMockBacklightAdapter).setBacklight(0.01f, 1f, 0.01f, 1f);
+        // ensure no greater than 100 strength is returned, therefore not called again.
+        verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+        verify(mMockedColorDisplayServiceInternal, times(6)).fetchEvenDimmerSpline(eq(3.0f));
+
+        // set up return to normal range
+        changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+                null);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        // verify return to normal range
+        verify(mMockBacklightAdapter, times(2)).setBacklight(0.35f, 500f, 0.35f, 500f);
+        verify(mMockedColorDisplayServiceInternal, times(2)).applyEvenDimmerColorChanges(eq(false),
+                anyInt());
+        verify(mMockedColorDisplayServiceInternal, times(7)).fetchEvenDimmerSpline(eq(3.0f));
+    }
+
     /**
      * Confirm that display is marked as private when it is listed in
      * com.android.internal.R.array.config_localPrivateDisplayPorts.
@@ -1461,15 +1599,16 @@
             return mSurfaceControlProxy;
         }
 
-        // Instead of using DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay)
-        // we should use DisplayDeviceConfig.create(context, isFirstDisplay) for the test to ensure
-        // that real device DisplayDeviceConfig is not loaded for FakeDisplay and we are getting
-        // consistent behaviour. Please also note that context passed to this method, is
-        // mMockContext and values will be loaded from mMockResources.
         @Override
         public DisplayDeviceConfig createDisplayDeviceConfig(Context context,
                 long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
-            return DisplayDeviceConfig.create(context, isFirstDisplay, flags);
+            return mMockDisplayDeviceConfig;
+        }
+
+        @Override
+        public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+                LocalDisplayAdapter.SurfaceControlProxy surfaceControlProxy) {
+            return mMockBacklightAdapter;
         }
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index 93066d8..67475335 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -213,7 +213,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(), any(),
+                any(), any(), any(),
                 anyLong(), anyLong());
 
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
@@ -277,7 +277,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null, null,
+                null, null, null, null,
                 0, 0);
 
         // Sleep until timeout should have triggered
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index 014b98c..43becc5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -216,7 +216,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(), any(),
+                any(), any(), any(),
                 anyLong(), anyLong());
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
         r.setPid(myPid());
@@ -265,7 +265,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null, null,
+                null, null, null, null,
                 0, 0);
         return app;
     }
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index fc4d8d8..0702926 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.power;
 
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -31,11 +34,13 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.display.AmbientDisplayConfiguration;
+import android.hardware.display.DisplayManagerInternal;
 import android.os.BatteryStats;
 import android.os.Handler;
 import android.os.IWakeLockCallback;
@@ -48,11 +53,18 @@
 import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.testing.TestableContext;
+import android.util.IntArray;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.DisplayAddress;
+import android.view.DisplayInfo;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.app.IBatteryStats;
 import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.power.batterysaver.BatterySaverStateMachine;
 import com.android.server.power.feature.PowerManagerFlags;
@@ -71,6 +83,8 @@
 public class NotifierTest {
     private static final String SYSTEM_PROPERTY_QUIESCENT = "ro.boot.quiescent";
     private static final int USER_ID = 0;
+    private static final int DISPLAY_PORT = 0xFF;
+    private static final long DISPLAY_MODEL = 0xEEEEEEEEL;
 
     @Mock private BatterySaverStateMachine mBatterySaverStateMachineMock;
     @Mock private PowerManagerService.NativeWrapper mNativeWrapperMock;
@@ -81,10 +95,16 @@
     @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
     @Mock private Vibrator mVibrator;
     @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+    @Mock private InputManagerInternal mInputManagerInternal;
+    @Mock private InputMethodManagerInternal mInputMethodManagerInternal;
+    @Mock private DisplayManagerInternal mDisplayManagerInternal;
+    @Mock private ActivityManagerInternal mActivityManagerInternal;
     @Mock private WakeLockLog mWakeLockLog;
 
     @Mock private IBatteryStats mBatteryStats;
 
+    @Mock private WindowManagerPolicy mPolicy;
+
     @Mock private PowerManagerFlags mPowerManagerFlags;
 
     @Mock private AppOpsManager mAppOpsManager;
@@ -96,6 +116,8 @@
     private FakeExecutor mTestExecutor = new FakeExecutor();
     private Notifier mNotifier;
 
+    private DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -103,11 +125,25 @@
         LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
         LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
 
+        LocalServices.removeServiceForTest(InputManagerInternal.class);
+        LocalServices.addService(InputManagerInternal.class, mInputManagerInternal);
+        LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
+        LocalServices.addService(InputMethodManagerInternal.class, mInputMethodManagerInternal);
+
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal);
+
+        mDefaultDisplayInfo.address = DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL);
+        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
+
         mContextSpy = spy(new TestableContext(InstrumentationRegistry.getContext()));
         mResourcesSpy = spy(mContextSpy.getResources());
         when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
         when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), anyString())).thenReturn("");
         when(mContextSpy.getSystemService(Vibrator.class)).thenReturn(mVibrator);
+        when(mDisplayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+                mDefaultDisplayInfo);
 
         mService = new PowerManagerService(mContextSpy, mInjector);
     }
@@ -232,6 +268,32 @@
     }
 
     @Test
+    public void testOnGlobalWakefulnessChangeStarted() throws Exception {
+        createNotifier();
+        // GIVEN system is currently non-interactive
+        when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+        final int displayId1 = 101;
+        final int displayId2 = 102;
+        final int[] displayIds = new int[]{displayId1, displayId2};
+        when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displayIds));
+        mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_ASLEEP,
+                PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, /* eventTime= */ 1000);
+        mTestLooper.dispatchAll();
+
+        // WHEN a global wakefulness change to interactive starts
+        mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_AWAKE,
+                PowerManager.WAKE_REASON_TAP, /* eventTime= */ 2000);
+        mTestLooper.dispatchAll();
+
+        // THEN input is notified of all displays being interactive
+        final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+        expectedDisplayInteractivities.put(displayId1, true);
+        expectedDisplayInteractivities.put(displayId2, true);
+        verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+        verify(mInputMethodManagerInternal).setInteractive(/* interactive= */ true);
+    }
+
+    @Test
     public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
         when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
         createNotifier();
@@ -551,7 +613,7 @@
                 mContextSpy,
                 mBatteryStats,
                 mInjector.createSuspendBlocker(mService, "testBlocker"),
-                null,
+                mPolicy,
                 null,
                 null,
                 mTestExecutor, mPowerManagerFlags, injector);
diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
index fd55428..c3df078 100644
--- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
+++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
@@ -32,4 +32,7 @@
     <device name="test.sensor.device">
         <subsystem>Sensor</subsystem>
     </device>
+    <device name="test.bluetooth.device">
+        <subsystem>Bluetooth</subsystem>
+    </device>
 </irq-device-map>
\ No newline at end of file
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 1d20538..c037f97 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -927,7 +927,7 @@
         assertThat(mPowerStatsStore.getTableOfContents()).isEmpty();
 
         mBatteryStatsImpl.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider,
-                mPowerStatsStore);
+                mPowerStatsStore, /* accumulateBatteryUsageStats */ false);
 
         synchronized (mBatteryStatsImpl) {
             mBatteryStatsImpl.noteFlashlightOnLocked(42, mMockClock.realtime, mMockClock.uptime);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index fde84e9..0e60156 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -419,7 +419,8 @@
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
                 mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
 
-        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ false);
         synchronized (batteryStats) {
             batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
@@ -505,6 +506,102 @@
                 .of(180.0);
     }
 
+    @Test
+    public void accumulateBatteryUsageStats() {
+        BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+        setTime(5 * MINUTE_IN_MS);
+
+        // Capture the session start timestamp
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        PowerStatsStore powerStatsStore = new PowerStatsStore(
+                new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
+                mStatsRule.getHandler());
+        powerStatsStore.reset();
+
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+                mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
+                mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+
+        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ true);
+
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+        }
+
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
+        }
+        setTime(55 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        // This section has not been saved yet, but should be added to the accumulated totals
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+        }
+        setTime(115 * MINUTE_IN_MS);
+
+        // Await completion
+        ConditionVariable done = new ConditionVariable();
+        mStatsRule.getHandler().post(done::open);
+        done.block();
+
+        BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
+                new BatteryUsageStatsQuery.Builder().accumulated().build());
+
+        assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
+        assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
+
+        // Section 1 (saved): 20 - 10 = 10
+        // Section 2 (saved): 50 - 30 = 20
+        // Section 3 (fresh): 110 - 80 = 30
+        // Total: 10 + 20 + 30 = 60
+        assertThat(stats.getAggregateBatteryConsumer(
+                        BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isWithin(0.0001)
+                .of(360.0);  // 360 mA * 1.0 hour
+        assertThat(stats.getAggregateBatteryConsumer(
+                        BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isEqualTo(60 * MINUTE_IN_MS);
+
+        final UidBatteryConsumer uidBatteryConsumer = stats.getUidBatteryConsumers().stream()
+                .filter(uid -> uid.getUid() == APP_UID).findFirst().get();
+        assertThat(uidBatteryConsumer
+                .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isWithin(0.1)
+                .of(360.0);
+        assertThat(uidBatteryConsumer
+                .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isEqualTo(60 * MINUTE_IN_MS);
+    }
+
     private void setTime(long timeMs) {
         mMockClock.currentTime = timeMs;
         mMockClock.realtime = timeMs;
@@ -550,7 +647,8 @@
             return null;
         }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
 
-        mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+        mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ false);
 
         // Make an incompatible change of supported energy components.  This will trigger
         // a BatteryStats reset, which will generate a snapshot of battery stats.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
index 0dc836b..fe4d971 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
@@ -17,6 +17,7 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -52,6 +53,7 @@
     private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device";
     private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device";
     private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device";
+    private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device";
     private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
     private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device";
     private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device";
@@ -62,12 +64,14 @@
     private static final int TEST_UID_3 = 92261423;
     private static final int TEST_UID_4 = 56926423;
     private static final int TEST_UID_5 = 76421423;
+    private static final int TEST_UID_6 = 62345353;
 
     private static final int TEST_PROC_STATE_1 = 72331;
     private static final int TEST_PROC_STATE_2 = 792351;
     private static final int TEST_PROC_STATE_3 = 138831;
     private static final int TEST_PROC_STATE_4 = 23231;
     private static final int TEST_PROC_STATE_5 = 42;
+    private static final int TEST_PROC_STATE_6 = 129942;
 
     private static final Context sContext = InstrumentationRegistry.getTargetContext();
     private final Handler mHandler = Mockito.mock(Handler.class);
@@ -79,6 +83,7 @@
         obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3);
         obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4);
         obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5);
+        obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6);
     }
 
     @Test
@@ -118,6 +123,7 @@
                 CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
                 CPU_WAKEUP_SUBSYSTEM_SENSOR,
                 CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+                CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
         };
 
         final String[] kernelReasons = new String[] {
@@ -126,10 +132,11 @@
                 KERNEL_REASON_SOUND_TRIGGER_IRQ,
                 KERNEL_REASON_SENSOR_IRQ,
                 KERNEL_REASON_CELLULAR_DATA_IRQ,
+                KERNEL_REASON_BLUETOOTH_IRQ,
         };
 
         final int[] uids = new int[] {
-                TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5
+                TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6
         };
 
         final int[] procStates = new int[] {
@@ -137,7 +144,8 @@
                 TEST_PROC_STATE_3,
                 TEST_PROC_STATE_4,
                 TEST_PROC_STATE_1,
-                TEST_PROC_STATE_5
+                TEST_PROC_STATE_5,
+                TEST_PROC_STATE_6
         };
 
         final int total = subsystems.length;
@@ -285,6 +293,40 @@
     }
 
     @Test
+    public void bluetoothIrqAttributionSolo() {
+        final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+        final long wakeupTime = 1236121;
+
+        populateDefaultProcStates(obj);
+
+        obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ);
+
+        // Outside the window, so should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2);
+        // Should be attributed
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3,
+                TEST_UID_5);
+
+        final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+        assertThat(attribution).isNotNull();
+        assertThat(attribution.size()).isEqualTo(1);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_1)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_2)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+                TEST_PROC_STATE_3);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_4)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+                TEST_PROC_STATE_5);
+    }
+
+    @Test
     public void alarmAndWifiIrqAttribution() {
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
         final long wakeupTime = 92123210;
@@ -400,6 +442,47 @@
     }
 
     @Test
+    public void unknownAndBluetoothAttribution() {
+        final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+        final long wakeupTime = 92123520;
+
+        populateDefaultProcStates(obj);
+
+        obj.noteWakeupTimeAndReason(wakeupTime, 24,
+                KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ);
+
+        // Bluetooth activity
+        // Outside the window, so should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4);
+        // Should be attributed
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3,
+                TEST_UID_5);
+
+        // Unrelated, should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+
+        final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+        assertThat(attribution).isNotNull();
+        assertThat(attribution.size()).isEqualTo(2);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo(
+                TEST_PROC_STATE_1);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+                .indexOfKey(TEST_UID_2)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+                TEST_PROC_STATE_3);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+                .indexOfKey(TEST_UID_4)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+                TEST_PROC_STATE_5);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull();
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse();
+    }
+
+    @Test
     public void unknownFormatWakeupIgnored() {
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
         final long wakeupTime = 72123210;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index bbe0755..cbe6700 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -52,8 +52,10 @@
         "services.credentials",
         "services.devicepolicy",
         "services.flags",
+        "com.android.server.flags.services-aconfig-java",
         "services.net",
         "services.people",
+        "services.supervision",
         "services.usage",
         "service-permission.stubs.system_server",
         "guava",
@@ -80,6 +82,7 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
+        "flag-junit",
         "junit",
         "junit-params",
         "ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
index ec78bce..c18faef 100644
--- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
@@ -31,6 +31,9 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.testing.TestableContext;
@@ -43,6 +46,9 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.flags.Flags;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
 import com.android.server.testutils.FakeDeviceConfigInterface;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
@@ -73,15 +79,18 @@
 
     private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
 
+    private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10;
+
     @Rule
     public TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private final ArraySet<String> mUpdatedPackages = new ArraySet<>();
     private ResolveInfo mHomePackageResolveInfo;
     private FakeDeviceConfigInterface mFakeDeviceConfigInterface;
     private PinnerService.Injector mInjector;
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -114,6 +123,8 @@
         resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false);
         resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0);
         resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false);
+        resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage,
+                MEMORY_PERCENTAGE_FOR_QUOTA);
 
         mFakeDeviceConfigInterface = new FakeDeviceConfigInterface();
         setDeviceConfigPinnedAnonSize(0);
@@ -138,10 +149,9 @@
             }
 
             @Override
-            protected PinnerService.PinnedFile pinFileInternal(String fileToPin,
-                    int maxBytesToPin, boolean attemptPinIntrospection) {
-                return new PinnerService.PinnedFile(-1,
-                        maxBytesToPin, fileToPin, maxBytesToPin);
+            protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+                    long maxBytesToPin, boolean attemptPinIntrospection) {
+                return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin);
             }
         };
     }
@@ -167,6 +177,12 @@
         unpinAnonRegionMethod.invoke(pinnerService);
     }
 
+    private long getGlobalPinQuota(PinnerService service) throws Exception {
+        Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota");
+        getQuotaMethod.setAccessible(true);
+        return (long) getQuotaMethod.invoke(service);
+    }
+
     private void waitForPinnerService(PinnerService pinnerService)
             throws NoSuchFieldException, IllegalAccessException {
         // There's no notification/callback when pinning finished
@@ -315,15 +331,121 @@
         PinnerService pinnerService = new PinnerService(mContext, mInjector);
         pinnerService.onStart();
 
-        pinnerService.pinFile("test_file", 4096, null, "my_group");
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
 
-        assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
-        assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0);
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(4096);
+        assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1);
 
         unpinAll(pinnerService);
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testPinAllQuota() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        long quota = getGlobalPinQuota(pinnerService);
+
+        pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false);
+
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(quota);
+
+        unpinAll(pinnerService);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinQuotaAsDevicePercentage() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+        long origQuota = getGlobalPinQuota(pinnerService);
+
+        long totalMem = android.os.Process.getTotalMemory();
+
+        // Verify that pin quota is the set percentage of device total memory
+        assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100);
+
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinWhenNoQuota() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // Verify that pin quota is zero
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+        assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0);
+    }
+
+    /**
+     * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix
+     * flag.
+     */
+    @Test
+    @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalQuotaDisabled() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // The quota parameter exists but it should have no effect on pinning
+        long quota = getGlobalPinQuota(pinnerService);
+
+        pinnerService.pinFile("test_file", quota + 1, null, "my_group", false);
+
+        // Verify that we can pin past the quota as it is disabled
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testUnpinReleasesQuota() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+        long origQuota = getGlobalPinQuota(pinnerService);
+
+        // Verify that pin quota exists and is non zero.
+        assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0);
+
+        pinnerService.pinFile("test_file", origQuota, null, "my_group", false);
+
+        // Make sure all the quota was consumed
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota);
+
+        // Unpin the file and verify that the quota has been released.
+        pinnerService.unpinFile("test_file");
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(0);
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinQuotaNegative() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // Verify that pin quota is zero
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+    }
+
+    @Test
     public void testPinAnonRegion() throws Exception {
         setDeviceConfigPinnedAnonSize(32768);
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
index 6e6d5a8..8dfd54f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
@@ -174,8 +174,8 @@
     @Mock private AccessibilityTrace mMockA11yTrace;
     @Mock private WindowManagerInternal mMockWindowManagerInternal;
     @Mock private SystemActionPerformer mMockSystemActionPerformer;
-    @Mock private IBinder mMockService;
-    @Mock private IAccessibilityServiceClient mMockServiceInterface;
+    @Mock private IBinder mMockClientBinder;
+    @Mock private IAccessibilityServiceClient mMockClient;
     @Mock private KeyEventDispatcher mMockKeyEventDispatcher;
     @Mock private IAccessibilityInteractionConnection mMockIA11yInteractionConnection;
     @Mock private IAccessibilityInteractionConnectionCallback mMockCallback;
@@ -247,9 +247,9 @@
                 mSpyServiceInfo, SERVICE_ID, mHandler, new Object(), mMockSecurityPolicy,
                 mMockSystemSupport, mMockA11yTrace, mMockWindowManagerInternal,
                 mMockSystemActionPerformer, mMockA11yWindowManager);
-        // Assume that the service is connected
-        mServiceConnection.mService = mMockService;
-        mServiceConnection.mServiceInterface = mMockServiceInterface;
+        // Assume that the client is connected
+        mServiceConnection.mClientBinder = mMockClientBinder;
+        mServiceConnection.mClient = mMockClient;
 
         // Update security policy for this service
         when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true);
@@ -273,7 +273,7 @@
         final KeyEvent mockKeyEvent = mock(KeyEvent.class);
 
         mServiceConnection.onKeyEvent(mockKeyEvent, sequenceNumber);
-        verify(mMockServiceInterface).onKeyEvent(mockKeyEvent, sequenceNumber);
+        verify(mMockClient).onKeyEvent(mockKeyEvent, sequenceNumber);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 566feb7..7481fc8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -63,8 +63,11 @@
 import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.annotation.NonNull;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
+import android.app.admin.DevicePolicyManager;
+import android.app.ecm.EnhancedConfirmationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -212,6 +215,7 @@
     @Mock private FullScreenMagnificationController mMockFullScreenMagnificationController;
     @Mock private ProxyManager mProxyManager;
     @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+    @Mock private DevicePolicyManager mDevicePolicyManager;
     @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback;
     @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;
     private IAccessibilityManager mA11yManagerServiceOnDevice;
@@ -241,6 +245,7 @@
                 UserManagerInternal.class, mMockUserManagerInternal);
         LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
         mInputFilter = mock(FakeInputFilter.class);
+        mTestableContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
 
         when(mMockMagnificationController.getMagnificationConnectionManager()).thenReturn(
                 mMockMagnificationConnectionManager);
@@ -2160,6 +2165,24 @@
                 .isEqualTo(SOFTWARE);
     }
 
+    @Test
+    @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED,
+            android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS})
+    public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() {
+        String fakePackageName = "FAKE_PACKAGE_NAME";
+        int uid = 0; // uid is not used in the actual implementation when flags are on
+        int userId = mTestableContext.getUserId() + 1234;
+        when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn(
+                List.of(fakePackageName));
+        Context mockUserContext = mock(Context.class);
+        mTestableContext.addMockUserContext(userId, mockUserContext);
+
+        mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId);
+
+        verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
+    }
+
+
     private Set<String> readStringsFromSetting(String setting) {
         final Set<String> result = new ArraySet<>();
         mA11yms.readColonDelimitedSettingToSet(
@@ -2280,6 +2303,7 @@
 
         private final Context mMockContext;
         private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
+        private ArrayMap<Integer, Context> mMockUserContexts = new ArrayMap<>();
 
         A11yTestableContext(Context base) {
             super(base);
@@ -2317,6 +2341,19 @@
             return mMockContext;
         }
 
+        public void addMockUserContext(int userId, Context context) {
+            mMockUserContexts.put(userId, context);
+        }
+
+        @Override
+        @NonNull
+        public Context createContextAsUser(UserHandle user, int flags) {
+            if (mMockUserContexts.containsKey(user.getIdentifier())) {
+                return mMockUserContexts.get(user.getIdentifier());
+            }
+            return super.createContextAsUser(user, flags);
+        }
+
         Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
             return mBroadcastReceivers;
         }
diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
index b9ce8ad..0c92abc 100644
--- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
@@ -1163,6 +1163,16 @@
 
         verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture());
         Bundle result = mBundleCaptor.getValue();
+        Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
+        assertNotNull(sessionBundle);
+        // Assert that session bundle is decrypted and hence data is visible.
+        assertEquals(AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1,
+                sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1));
+        // Assert finishSessionAsUser added calling uid and pid into the sessionBundle
+        assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_UID));
+        assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_PID));
+        assertEquals(sessionBundle.getString(
+                AccountManager.KEY_ANDROID_PACKAGE_NAME), "APCT.package");
 
         // Verify response data
         assertNull(result.getString(AccountManager.KEY_AUTHTOKEN, null));
@@ -2111,6 +2121,12 @@
                 result.getString(AccountManager.KEY_ACCOUNT_NAME));
         assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1,
                 result.getString(AccountManager.KEY_ACCOUNT_TYPE));
+
+        Bundle optionBundle = result.getParcelable(
+                AccountManagerServiceTestFixtures.KEY_OPTIONS_BUNDLE);
+        // Assert addAccountAsUser added calling uid and pid into the option bundle
+        assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_UID));
+        assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_PID));
     }
 
     @SmallTest
@@ -3441,52 +3457,6 @@
                 + (readTotalTime.doubleValue() / readerCount / loopSize));
     }
 
-    @SmallTest
-    public void testSanitizeBundle_expectedFields() throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name");
-        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, "type");
-        bundle.putString(AccountManager.KEY_AUTHTOKEN, "token");
-        bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, "label");
-        bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "error message");
-        bundle.putString(AccountManager.KEY_PASSWORD, "password");
-        bundle.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN, "status");
-
-        bundle.putLong(AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 123L);
-        bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
-        bundle.putInt(AccountManager.KEY_ERROR_CODE, 456);
-
-        Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle);
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_TYPE), "type");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTHTOKEN), "token");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTH_TOKEN_LABEL), "label");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_ERROR_MESSAGE), "error message");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_PASSWORD), "password");
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN), "status");
-
-        assertEquals(sanitizedBundle.getLong(
-                AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 0), 123L);
-        assertEquals(sanitizedBundle.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false), true);
-        assertEquals(sanitizedBundle.getInt(AccountManager.KEY_ERROR_CODE, 0), 456);
-    }
-
-    @SmallTest
-    public void testSanitizeBundle_filtersUnexpectedFields() throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name");
-        bundle.putString("unknown_key", "value");
-        Bundle sessionBundle = new Bundle();
-        bundle.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
-
-        Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle);
-
-        assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name");
-        assertFalse(sanitizedBundle.containsKey("unknown_key"));
-        // It is a valid response from Authenticator which will be accessed using original Bundle
-        assertFalse(sanitizedBundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE));
-    }
-
     private void waitForCyclicBarrier(CyclicBarrier cyclicBarrier) {
         try {
             cyclicBarrier.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 4645156..3e2949d6 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -16,7 +16,9 @@
 
 package com.android.server.audio;
 
+import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
 import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
 
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
@@ -116,7 +118,7 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+    @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX})
     public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception {
         AudioManager am = mContext.getSystemService(AudioManager.class);
         final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
@@ -177,6 +179,7 @@
         final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
                 /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
         final int maxPreScaleIndex = 3;
+        int passedIndex = maxIndex;
 
         for (int i = 0; i < maxPreScaleIndex; i++) {
             final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -185,9 +188,12 @@
             mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName);
             mTestLooper.dispatchAll();
 
+            if (absVolumeIndexFix()) {
+                passedIndex = i + 1;
+            }
             // Stream volume changes
             verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
-                            AudioManager.STREAM_MUSIC, maxIndex,
+                            AudioManager.STREAM_MUSIC, passedIndex,
                             AudioSystem.DEVICE_OUT_BLE_HEADSET);
         }
 
@@ -197,8 +203,11 @@
         mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName);
         mTestLooper.dispatchAll();
 
+        if (absVolumeIndexFix()) {
+            passedIndex = 4;
+        }
         verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
-                        AudioManager.STREAM_MUSIC, maxIndex,
+                        AudioManager.STREAM_MUSIC, passedIndex,
                         AudioSystem.DEVICE_OUT_BLE_HEADSET);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index beed0a3d..c305fd9 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -39,8 +39,9 @@
 import static android.view.KeyEvent.ACTION_DOWN;
 import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
 
-import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
 import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
+import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -627,7 +628,6 @@
 
     @Test
     @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
-    @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX)
     public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception {
         final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
         final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
@@ -638,6 +638,7 @@
         final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
                 /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
         final int maxPreScaleIndex = 3;
+        int passedIndex = maxIndex;
 
         for (int i = 0; i < maxPreScaleIndex; i++) {
             final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -646,9 +647,12 @@
             mAudioService.setDeviceVolume(volCur, bleDevice, mContext.getOpPackageName());
             mTestLooper.dispatchAll();
 
+            if (absVolumeIndexFix()) {
+                passedIndex = i + 1;
+            }
             // Stream volume changes
             verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
-                    STREAM_MUSIC, maxIndex,
+                    STREAM_MUSIC, passedIndex,
                     AudioSystem.DEVICE_OUT_BLE_HEADSET);
         }
 
@@ -658,8 +662,11 @@
         mAudioService.setDeviceVolume(volIndex4, bleDevice, mContext.getOpPackageName());
         mTestLooper.dispatchAll();
 
+        if (absVolumeIndexFix()) {
+            passedIndex = 4;
+        }
         verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
-                STREAM_MUSIC, maxIndex,
+                STREAM_MUSIC, passedIndex,
                 AudioSystem.DEVICE_OUT_BLE_HEADSET);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index 14cb22d..efc2d97 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -16,12 +16,20 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+
+import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
@@ -36,8 +44,12 @@
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 @Presubmit
 @SmallTest
@@ -45,6 +57,17 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
+    public MockitoRule mockitorule = MockitoJUnit.rule();
+
+    @Mock
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+    }
 
     @Test
     public void testCombineAuthenticatorBundles_withKeyDeviceCredential_andKeyAuthenticators() {
@@ -162,28 +185,39 @@
 
     @Test
     public void testIsValidAuthenticatorConfig() {
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.EMPTY_SET));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.EMPTY_SET));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_STRONG));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_WEAK));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_WEAK));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_WEAK));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_CONVENIENCE));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE
+        assertFalse(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_CONVENIENCE
                 | Authenticators.DEVICE_CREDENTIAL));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MAX_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MAX_STRENGTH));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MIN_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MIN_STRENGTH));
+
+        assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig(
+                        mContext, Authenticators.MANDATORY_BIOMETRICS));
+
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.MANDATORY_BIOMETRICS));
 
         // The rest of the bits are not allowed to integrate with the public APIs
         for (int i = 8; i < 32; i++) {
@@ -192,7 +226,7 @@
                     || authenticator == Authenticators.MANDATORY_BIOMETRICS) {
                 continue;
             }
-            assertFalse(Utils.isValidAuthenticatorConfig(1 << i));
+            assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i));
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
index d071c15..ae781dc 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -60,6 +60,7 @@
 import android.os.ServiceSpecificException;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+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;
@@ -130,6 +131,7 @@
     private SecretKey mAesKey;
     private MockInjector mMockInjector;
     private Handler mHandler;
+    private Network mNetwork;
 
     public interface MockableRebootEscrowInjected {
         int getBootCount();
@@ -342,6 +344,7 @@
         when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false);
         when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
         mInjected = mock(MockableRebootEscrowInjected.class);
+        mNetwork = mock(Network.class);
         mMockInjector =
                 new MockInjector(
                         mContext,
@@ -351,6 +354,10 @@
                         mKeyStoreManager,
                         mStorage,
                         mInjected);
+        mMockInjector.mNetworkConsumer =
+                (callback) -> {
+                    callback.onAvailable(mNetwork);
+                };
         HandlerThread thread = new HandlerThread("RebootEscrowManagerTest");
         thread.start();
         mHandler = new Handler(thread.getLooper());
@@ -367,6 +374,10 @@
                         mKeyStoreManager,
                         mStorage,
                         mInjected);
+        mMockInjector.mNetworkConsumer =
+                (callback) -> {
+                    callback.onAvailable(mNetwork);
+                };
         mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
     }
 
@@ -621,7 +632,7 @@
         // pretend reboot happens here
         when(mInjected.getBootCount()).thenReturn(1);
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection, never()).unwrap(any(), anyLong());
         verify(mCallbacks, never()).onRebootEscrowRestored(anyByte(), any(), anyInt());
     }
@@ -678,7 +689,7 @@
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
 
         verify(mServiceConnection).unwrap(any(), anyLong());
         verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
@@ -734,7 +745,7 @@
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
 
         verify(mServiceConnection).unwrap(any(), anyLong());
         verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
@@ -783,7 +794,7 @@
 
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection).unwrap(any(), anyLong());
         assertTrue(metricsSuccessCaptor.getValue());
         verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
@@ -827,7 +838,7 @@
                         anyInt());
 
         when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class);
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection).unwrap(any(), anyLong());
         assertFalse(metricsSuccessCaptor.getValue());
         assertEquals(
@@ -836,6 +847,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
     public void loadRebootEscrowDataIfAvailable_ServerBasedIoError_RetryFailure() throws Exception {
         setServerBasedRebootEscrowProvider();
 
@@ -930,114 +942,6 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
-    public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternet_success()
-            throws Exception {
-        setServerBasedRebootEscrowProvider();
-
-        when(mInjected.getBootCount()).thenReturn(0);
-        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
-        mService.setRebootEscrowListener(mockListener);
-        mService.prepareRebootEscrow();
-
-        clearInvocations(mServiceConnection);
-        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
-        verify(mockListener).onPreparedForReboot(eq(true));
-        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
-
-        // Use x -> x for both wrap & unwrap functions.
-        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
-        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
-        assertTrue(mStorage.hasRebootEscrowServerBlob());
-
-        // pretend reboot happens here
-        when(mInjected.getBootCount()).thenReturn(1);
-        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
-        doNothing()
-                .when(mInjected)
-                .reportMetric(
-                        metricsSuccessCaptor.capture(),
-                        eq(0) /* error code */,
-                        eq(2) /* Server based */,
-                        eq(1) /* attempt count */,
-                        anyInt(),
-                        eq(0) /* vbmeta status */,
-                        anyInt());
-
-        // load escrow data
-        when(mServiceConnection.unwrap(any(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        Network mockNetwork = mock(Network.class);
-        mMockInjector.mNetworkConsumer =
-                (callback) -> {
-                    callback.onAvailable(mockNetwork);
-                };
-
-        mService.loadRebootEscrowDataIfAvailable(mHandler);
-        verify(mServiceConnection).unwrap(any(), anyLong());
-        assertTrue(metricsSuccessCaptor.getValue());
-        verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
-        assertNull(mMockInjector.mNetworkCallback);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
-    public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternetRemoteException_Failure()
-            throws Exception {
-        setServerBasedRebootEscrowProvider();
-
-        when(mInjected.getBootCount()).thenReturn(0);
-        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
-        mService.setRebootEscrowListener(mockListener);
-        mService.prepareRebootEscrow();
-
-        clearInvocations(mServiceConnection);
-        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
-        verify(mockListener).onPreparedForReboot(eq(true));
-        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
-
-        // Use x -> x for both wrap & unwrap functions.
-        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
-        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
-        assertTrue(mStorage.hasRebootEscrowServerBlob());
-
-        // pretend reboot happens here
-        when(mInjected.getBootCount()).thenReturn(1);
-        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
-        ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class);
-        doNothing()
-                .when(mInjected)
-                .reportMetric(
-                        metricsSuccessCaptor.capture(),
-                        metricsErrorCodeCaptor.capture(),
-                        eq(2) /* Server based */,
-                        eq(1) /* attempt count */,
-                        anyInt(),
-                        eq(0) /* vbmeta status */,
-                        anyInt());
-
-        // load escrow data
-        when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class);
-        Network mockNetwork = mock(Network.class);
-        mMockInjector.mNetworkConsumer =
-                (callback) -> {
-                    callback.onAvailable(mockNetwork);
-                };
-
-        mService.loadRebootEscrowDataIfAvailable(mHandler);
-        verify(mServiceConnection).unwrap(any(), anyLong());
-        assertFalse(metricsSuccessCaptor.getValue());
-        assertEquals(
-                Integer.valueOf(RebootEscrowManager.ERROR_LOAD_ESCROW_KEY),
-                metricsErrorCodeCaptor.getValue());
-        assertNull(mMockInjector.mNetworkCallback);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
     public void loadRebootEscrowDataIfAvailable_waitForInternet_networkUnavailable()
             throws Exception {
         setServerBasedRebootEscrowProvider();
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index abc9ce3..ee63d5d 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -91,6 +92,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -174,8 +176,8 @@
     private PackageManager mPackageManager;
     @Mock
     private KeyguardManager mKeyguardManager;
-    @Mock
-    AppOpsManager mAppOpsManager;
+
+    private AppOpsManager mAppOpsManager;
     @Mock
     private IMediaProjectionWatcherCallback mWatcherCallback;
     @Mock
@@ -193,6 +195,7 @@
         LocalServices.removeServiceForTest(WindowManagerInternal.class);
         LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
 
+        mAppOpsManager = mockAppOpsManager();
         mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
         mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
         mContext.setMockPackageManager(mPackageManager);
@@ -206,6 +209,17 @@
         mService = new MediaProjectionManagerService(mContext);
     }
 
+    private static AppOpsManager mockAppOpsManager() {
+        return mock(AppOpsManager.class, invocationOnMock -> {
+            if (invocationOnMock.getMethod().getName().startsWith("noteOp")) {
+                // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED
+                // and is not what we want.
+                return AppOpsManager.MODE_IGNORED;
+            }
+            return Answers.RETURNS_DEFAULTS.answer(invocationOnMock);
+        });
+    }
+
     @After
     public void tearDown() {
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
@@ -305,8 +319,10 @@
     public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
             throws NameNotFoundException {
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
-        doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA),
-                eq(projection.uid), eq(projection.packageName));
+        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager)
+                .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA),
+                        eq(projection.uid), eq(projection.packageName), nullable(String.class),
+                        nullable(String.class));
         doReturn(true).when(mKeyguardManager).isKeyguardLocked();
 
         doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
@@ -1159,7 +1175,7 @@
         doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
                 any(ApplicationInfoFlags.class), any(UserHandle.class));
         return service.createProjectionInternal(UID, PACKAGE_NAME,
-                TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
+                TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT);
     }
 
     // Set up preconditions for starting a projection, with no foreground service requirements.
diff --git a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
index f91f77a..cdfc521 100644
--- a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
@@ -86,7 +86,6 @@
     private File mDataBlockFile;
     private File mFrpSecretFile;
     private File mFrpSecretTmpFile;
-    private String mOemUnlockPropertyValue;
     private boolean mIsUpgradingFromPreV = false;
 
     @Mock private UserManager mUserManager;
@@ -105,13 +104,6 @@
         }
 
         @Override
-        void setProperty(String key, String value) {
-            // Override to capture the value instead of actually setting the property.
-            assertThat(key).isEqualTo("sys.oem_unlock_allowed");
-            mOemUnlockPropertyValue = value;
-        }
-
-        @Override
         boolean isUpgradingFromPreVRelease() {
             return mIsUpgradingFromPreV;
         }
@@ -598,7 +590,6 @@
 
         mInterface.setOemUnlockEnabled(true);
         assertThat(mInterface.getOemUnlockEnabled()).isTrue();
-        assertThat(mOemUnlockPropertyValue).isEqualTo("1");
     }
 
     @Test
@@ -635,7 +626,6 @@
         // The current implementation does not check digest before set or get the oem unlock bit.
         tamperWithDigest();
         mInterface.setOemUnlockEnabled(true);
-        assertThat(mOemUnlockPropertyValue).isEqualTo("1");
         tamperWithDigest();
         assertThat(mInterface.getOemUnlockEnabled()).isTrue();
     }
@@ -676,7 +666,6 @@
 
         mInternalInterface.forceOemUnlockEnabled(true);
 
-        assertThat(mOemUnlockPropertyValue).isEqualTo("1");
         assertThat(readBackingFile(mPdbService.getOemUnlockDataOffset(), 1).array())
                 .isEqualTo(new byte[] { 1 });
     }
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
new file mode 100644
index 0000000..6bd4279
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.server.supervision
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.server.LocalServices
+import com.android.server.pm.UserManagerInternal
+import com.google.common.truth.Truth.assertThat
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+/**
+ * Unit tests for {@link SupervisionService}.
+ * <p/>
+ * Run with <code>atest SupervisionServiceTest</code>.
+ */
+@RunWith(AndroidJUnit4::class)
+class SupervisionServiceTest {
+    companion object {
+        const val USER_ID = 100
+    }
+
+    private lateinit var service: SupervisionService
+
+    @Rule
+    @JvmField
+    val mocks: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var mockUserManagerInternal: UserManagerInternal
+
+    @Before
+    fun setup() {
+        val context = InstrumentationRegistry.getInstrumentation().context
+
+        LocalServices.removeServiceForTest(UserManagerInternal::class.java)
+        LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
+
+        service = SupervisionService(context)
+    }
+
+    @Test
+    fun testSetSupervisionEnabledForUser() {
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+        service.setSupervisionEnabledForUser(USER_ID, true)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+        service.setSupervisionEnabledForUser(USER_ID, false)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+    }
+
+    @Test
+    fun testSetSupervisionLockscreenEnabledForUser() {
+        var userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isFalse()
+        assertThat(userData.supervisionLockScreenOptions).isNull()
+
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+        userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isTrue()
+        assertThat(userData.supervisionLockScreenOptions).isNotNull()
+
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, false, null)
+        userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isFalse()
+        assertThat(userData.supervisionLockScreenOptions).isNull()
+    }
+}
diff --git a/services/tests/timetests/Android.bp b/services/tests/timetests/Android.bp
index aae6acc..65a694e 100644
--- a/services/tests/timetests/Android.bp
+++ b/services/tests/timetests/Android.bp
@@ -19,6 +19,7 @@
         "platform-test-annotations",
         "services.core",
         "truth",
+        "ApplicationSharedMemoryTestRule",
     ],
     libs: ["android.test.runner.stubs.system"],
     platform_apis: true,
diff --git a/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
index c64ec72..3836063 100644
--- a/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -46,6 +46,7 @@
 import android.os.TimestampedValue;
 import android.util.IndentingPrintWriter;
 
+import com.android.internal.os.ApplicationSharedMemoryTestRule;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timedetector.TimeDetectorStrategy.Origin;
 import com.android.server.timezonedetector.StateChangeListener;
@@ -55,6 +56,7 @@
 import junitparams.Parameters;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -125,6 +127,10 @@
                     .setAutoDetectionEnabledSetting(true)
                     .build();
 
+    @Rule
+    public final ApplicationSharedMemoryTestRule mApplicationSharedMemoryTestRule =
+            new ApplicationSharedMemoryTestRule();
+
     private FakeEnvironment mFakeEnvironment;
     private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private TimeDetectorStrategyImpl mTimeDetectorStrategy;
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 130690d..bbf2cbd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -193,6 +193,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
 import android.app.PendingIntent;
 import android.app.Person;
 import android.app.RemoteInput;
@@ -655,7 +656,8 @@
         when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt()))
                 .thenReturn(INVALID_TASK_ID);
         mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
-        when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId });
+        when(mUm.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
+        when(mUmInternal.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
         when(mAmi.getCurrentUserId()).thenReturn(mUserId);
 
         when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true);
@@ -4652,7 +4654,42 @@
 
         doThrow(new SecurityException("no access")).when(mUgmInternal)
                 .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri),
-                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+                anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        mBinderService.updateNotificationChannelFromPrivilegedListener(
+                null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
+
+        verify(mPreferencesHelper, times(1)).updateNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(),  anyInt(), anyBoolean());
+
+        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
+                eq(Process.myUserHandle()), eq(mTestNotificationChannel),
+                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+    }
+
+    @Test
+    public void
+        testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
+            throws Exception {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(singletonList(mock(AssociationInfo.class)));
+        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
+                eq(mTestNotificationChannel.getId()), anyBoolean()))
+                .thenReturn(mTestNotificationChannel);
+
+        // Missing Uri permissions for the old channel sound
+        final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri),
+                anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        // Has Uri permissions for the old channel sound
+        final Uri newSoundUri = Uri.parse("content://media/test/sound/uri");
+        final NotificationChannel updatedNotificationChannel = new NotificationChannel(
+                TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+        updatedNotificationChannel.setSound(newSoundUri,
+                updatedNotificationChannel.getAudioAttributes());
 
         mBinderService.updateNotificationChannelFromPrivilegedListener(
                 null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
@@ -15936,6 +15973,57 @@
         assertThat(updatedRule.getValue().isEnabled()).isFalse();
     }
 
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+
+        // The caller will supply states with "wrong" hasPriorityChannels.
+        int stateBlockingPriorityChannels = Policy.policyState(false, false);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, stateBlockingPriorityChannels, 0), false);
+
+        // hasPriorityChannels is untouched and allowPriorityChannels was updated.
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, false));
+
+        // Same but setting allowPriorityChannels to true.
+        int stateAllowingPriorityChannels = Policy.policyState(false, true);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(2, 0, 0, 0, stateAllowingPriorityChannels, 0), false);
+
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(2);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+        mService.setCallerIsNormalPackage();
+
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, Policy.policyState(false, false), 0), false);
+
+        // Policy was updated but the attempt to change state was ignored (it's a @hide API).
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
     /** Prepares for a zen-related test that uses the real {@link ZenModeHelper}. */
     private void setUpRealZenTest() throws Exception {
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
@@ -16556,7 +16644,7 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted_granted() throws Exception {
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16611,7 +16699,7 @@
         mService.addNotification(r);
         mService.addEnqueuedNotification(r1);
 
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
 
         waitForIdle();
 
@@ -16632,7 +16720,7 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception {
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16651,9 +16739,9 @@
 
         mService.addNotification(r);
 
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
         waitForIdle();
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
         waitForIdle();
 
         ArgumentCaptor<NotificationRecord> captor =
@@ -16663,12 +16751,12 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted_revoked() throws Exception {
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
         // start from true state
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
 
         // qualifying posted notification
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16709,7 +16797,7 @@
         mService.addNotification(r);
         mService.addEnqueuedNotification(r1);
 
-        mBinderService.setCanBePromoted(mPkg, mUid, false);
+        mBinderService.setCanBePromoted(mPkg, mUid, false, true);
 
         waitForIdle();
 
@@ -16728,12 +16816,12 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted_revoked_onlyNotifiesOnce() throws Exception {
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
         // start from true state
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
 
         // qualifying posted notification
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16751,9 +16839,9 @@
 
         mService.addNotification(r);
 
-        mBinderService.setCanBePromoted(mPkg, mUid, false);
+        mBinderService.setCanBePromoted(mPkg, mUid, false, true);
         waitForIdle();
-        mBinderService.setCanBePromoted(mPkg, mUid, false);
+        mBinderService.setCanBePromoted(mPkg, mUid, false, true);
         waitForIdle();
 
         ArgumentCaptor<NotificationRecord> captor =
@@ -16763,10 +16851,10 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testPostPromotableNotification() throws Exception {
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
-        assertThat(mBinderService.canBePromoted(mPkg, mUid)).isTrue();
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
+        assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isTrue();
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
 
@@ -16776,7 +16864,6 @@
                 .setColor(Color.WHITE)
                 .setColorized(true)
                 .build();
-        //assertThat(n.hasPromotableCharacteristics()).isTrue();
         StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0,
                 n, UserHandle.getUserHandleForUid(mUid), null, 0);
 
@@ -16794,7 +16881,7 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testPostPromotableNotification_noPermission() throws Exception {
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16822,9 +16909,9 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testPostPromotableNotification_unimportantNotification() throws Exception {
-        mBinderService.setCanBePromoted(mPkg, mUid, true);
+        mBinderService.setCanBePromoted(mPkg, mUid, true, true);
         mContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
         Notification n = new Notification.Builder(mContext, mMinChannel.getId())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 7d63062..d64b9e8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -45,11 +45,13 @@
 import static android.app.NotificationManager.IMPORTANCE_NONE;
 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
 import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE;
+import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
 import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_SYSTEM;
-
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
 import static android.service.notification.Flags.notificationClassification;
@@ -59,9 +61,11 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
 import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI;
 import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
 import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
 import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
 import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT;
 import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT;
 import static com.android.server.notification.PreferencesHelper.UNKNOWN_UID;
@@ -83,6 +87,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -368,10 +373,10 @@
 
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         resetZenModeHelper();
 
         mAudioAttributes = new AudioAttributes.Builder()
@@ -643,7 +648,7 @@
 
         mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true);
         if (android.app.Flags.uiRichOngoing()) {
-            mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true);
+            mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true, true);
         }
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false,
@@ -657,6 +662,8 @@
         assertTrue(mXmlHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
         if (android.app.Flags.uiRichOngoing()) {
             assertThat(mXmlHelper.canBePromoted(PKG_N_MR1, UID_N_MR1)).isTrue();
+            assertThat(mXmlHelper.getAppLockedFields(PKG_N_MR1, UID_N_MR1) & USER_LOCKED_PROMOTABLE)
+                    .isNotEqualTo(0);
         }
         assertEquals(channel1,
                 mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false));
@@ -780,7 +787,7 @@
     public void testReadXml_oldXml_migrates() throws Exception {
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"2\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1
@@ -916,7 +923,7 @@
     public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -975,7 +982,7 @@
     public void testReadXml_newXml_permissionNotificationOff() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ false, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1034,7 +1041,7 @@
     public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"4\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1706,7 +1713,7 @@
         // simulate load after reboot
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
 
         // Trigger 2nd restore pass
@@ -1761,7 +1768,7 @@
         // simulate load after reboot
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         loadByteArrayXml(xml.getBytes(), false, USER_ALL);
 
         // Trigger 2nd restore pass
@@ -1839,10 +1846,10 @@
 
         mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
 
         NotificationChannel channel =
                 new NotificationChannel("id", "name", IMPORTANCE_LOW);
@@ -3046,6 +3053,64 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() {
+        final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        assertThrows(SecurityException.class,
+                () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel,
+                    true, false, UID_N_MR1, false));
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() {
+        final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+                .getSound()).isEqualTo(sound);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() {
+        final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+                .getSound()).isEqualTo(sound);
+    }
+
+    @Test
     public void testPermanentlyDeleteChannels() throws Exception {
         NotificationChannel channel1 =
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
@@ -6311,20 +6376,30 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testNoAppHasPermissionToPromoteByDefault() {
         mHelper.setShowBadge(PKG_P, UID_P, true);
         assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted() {
-        mHelper.setCanBePromoted(PKG_P, UID_P, true);
+        mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
         assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
 
-        mHelper.setCanBePromoted(PKG_P, UID_P, false);
+        mHelper.setCanBePromoted(PKG_P, UID_P, false, true);
         assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
         verify(mHandler, never()).requestSort();
     }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
+    public void testSetCanBePromoted_allowlistNotOverrideUser() {
+        mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
+        assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+
+        mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
+        assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+    }
 }
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index c7a136a..23ee893 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -44,7 +44,8 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -52,12 +53,14 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.IActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManagerInternal;
 import android.media.AudioManager;
 import android.os.Handler;
@@ -79,12 +82,10 @@
 
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
-import com.android.server.LocalServices;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.vibrator.VibrationSession.CallerInfo;
 import com.android.server.vibrator.VibrationSession.Status;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -101,8 +102,7 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
-    private static final int OLD_USER_ID = 123;
-    private static final int NEW_USER_ID = 456;
+    private static final int USER_ID = 123;
     private static final int UID = 1;
     private static final int VIRTUAL_DEVICE_ID = 1;
     private static final String SYSUI_PACKAGE_NAME = "sysui";
@@ -132,13 +132,12 @@
     @Mock private VirtualDeviceManagerInternal mVirtualDeviceManagerInternalMock;
     @Mock private PackageManagerInternal mPackageManagerInternalMock;
     @Mock private AudioManager mAudioManagerMock;
+    @Mock private IActivityManager mActivityManagerMock;
     @Mock private VibrationConfig mVibrationConfigMock;
 
     private TestLooper mTestLooper;
     private ContextWrapper mContextSpy;
     private VibrationSettings mVibrationSettings;
-    private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
-    private BroadcastReceiver mRegisteredBatteryBroadcastReceiver;
 
     @Before
     public void setUp() throws Exception {
@@ -146,24 +145,21 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
 
         ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
-        when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
-        when(mContextSpy.getSystemService(eq(Context.AUDIO_SERVICE))).thenReturn(mAudioManagerMock);
-        doAnswer(invocation -> {
-            mRegisteredPowerModeListener = invocation.getArgument(0);
-            return null;
-        }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any());
+        doReturn(contentResolver).when(mContextSpy).getContentResolver();
+
+        // Make sure broadcast receivers are not registered for this test, to avoid flakes.
+        doReturn(null).when(mContextSpy)
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+
         when(mPackageManagerInternalMock.getSystemUiServiceComponent())
                 .thenReturn(new ComponentName(SYSUI_PACKAGE_NAME, ""));
 
-        removeServicesForTest();
-        addServicesForTest();
-
         setDefaultIntensity(VIBRATION_INTENSITY_MEDIUM);
 
         setIgnoreVibrationsOnWirelessCharger(false);
-        createSystemReadyVibrationSettings();
-
         mockGoToSleep(/* goToSleepTime= */ 0, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+
+        createSystemReadyVibrationSettings();
     }
 
     private void createSystemReadyVibrationSettings() {
@@ -177,37 +173,18 @@
         setUserSetting(Settings.System.APPLY_RAMPING_RINGER, 0);
         setRingerMode(AudioManager.RINGER_MODE_NORMAL);
 
-        mVibrationSettings.onSystemReady();
-    }
-
-    private void removeServicesForTest() {
-        LocalServices.removeServiceForTest(PowerManagerInternal.class);
-        LocalServices.removeServiceForTest(PackageManagerInternal.class);
-        LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
-    }
-
-    private void addServicesForTest() {
-        LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);
-        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
-        LocalServices.addService(VirtualDeviceManagerInternal.class,
-                mVirtualDeviceManagerInternalMock);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        removeServicesForTest();
+        mVibrationSettings.onSystemReady(mPackageManagerInternalMock, mPowerManagerInternalMock,
+                mActivityManagerMock, mVirtualDeviceManagerInternalMock, mAudioManagerMock);
     }
 
     @Test
     public void create_withOnlyRequiredSystemServices() {
-        // The only core services that we depend on are PowerManager and PackageManager
-        removeServicesForTest();
-        LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);
-        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
-
         VibrationSettings minimalVibrationSettings = new VibrationSettings(mContextSpy,
                 new Handler(mTestLooper.getLooper()), mVibrationConfigMock);
-        minimalVibrationSettings.onSystemReady();
+
+        // The only core services that we depend on are Power, Package and Activity managers
+        minimalVibrationSettings.onSystemReady(mPackageManagerInternalMock,
+                mPowerManagerInternalMock, mActivityManagerMock, null, null);
     }
 
     @Test
@@ -215,8 +192,8 @@
         mVibrationSettings.addListener(mListenerMock);
 
         // Testing the broadcast flow manually.
-        mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID);
-        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
+        mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID);
+        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
 
         verify(mListenerMock, times(2)).onChange();
     }
@@ -226,9 +203,9 @@
         mVibrationSettings.addListener(mListenerMock);
 
         // Testing the broadcast flow manually.
-        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+        mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
                 new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
-        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+        mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
                 new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
 
         verify(mListenerMock, times(2)).onChange();
@@ -250,9 +227,9 @@
         mVibrationSettings.addListener(mListenerMock);
 
         // Testing the broadcast flow manually.
-        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
-        mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
-        mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // No change.
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // Noop.
 
         verify(mListenerMock, times(2)).onChange();
     }
@@ -268,9 +245,9 @@
 
         // Trigger multiple observers manually.
         mVibrationSettings.mSettingObserver.onChange(false);
-        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
-        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
-        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
+        mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
                 new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
 
         verifyNoMoreInteractions(mListenerMock);
@@ -311,11 +288,12 @@
 
     @Test
     public void wirelessChargingVibrationsEnabled_doesNotRegisterBatteryReceiver_allowsAnyUsage() {
-        setBatteryReceiverRegistrationResult(getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS));
         setIgnoreVibrationsOnWirelessCharger(false);
         createSystemReadyVibrationSettings();
 
-        assertNull(mRegisteredBatteryBroadcastReceiver);
+        verify(mContextSpy, never()).registerReceiver(any(BroadcastReceiver.class),
+                argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
         for (int usage : ALL_USAGES) {
             assertVibrationNotIgnoredForUsage(usage);
         }
@@ -323,7 +301,6 @@
 
     @Test
     public void shouldIgnoreVibration_noBatteryIntentWhenSystemReady_allowsAnyUsage() {
-        setBatteryReceiverRegistrationResult(null);
         setIgnoreVibrationsOnWirelessCharger(true);
         createSystemReadyVibrationSettings();
 
@@ -335,7 +312,10 @@
     @Test
     public void shouldIgnoreVibration_onNonWirelessChargerWhenSystemReady_allowsAnyUsage() {
         Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
-        setBatteryReceiverRegistrationResult(nonWirelessChargingIntent);
+        doReturn(nonWirelessChargingIntent).when(mContextSpy).registerReceiver(
+                any(BroadcastReceiver.class),
+                argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
         setIgnoreVibrationsOnWirelessCharger(true);
         createSystemReadyVibrationSettings();
 
@@ -347,7 +327,10 @@
     @Test
     public void shouldIgnoreVibration_onWirelessChargerWhenSystemReady_doesNotAllowFromAnyUsage() {
         Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
-        setBatteryReceiverRegistrationResult(wirelessChargingIntent);
+        doReturn(wirelessChargingIntent).when(mContextSpy).registerReceiver(
+                any(BroadcastReceiver.class),
+                argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
         setIgnoreVibrationsOnWirelessCharger(true);
         createSystemReadyVibrationSettings();
 
@@ -358,13 +341,12 @@
 
     @Test
     public void shouldIgnoreVibration_receivesWirelessChargingIntent_doesNotAllowFromAnyUsage() {
-        Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
-        setBatteryReceiverRegistrationResult(nonWirelessChargingIntent);
         setIgnoreVibrationsOnWirelessCharger(true);
         createSystemReadyVibrationSettings();
 
         Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
-        mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, wirelessChargingIntent);
+        mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+                mContextSpy, wirelessChargingIntent);
 
         for (int usage : ALL_USAGES) {
             assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER);
@@ -373,17 +355,21 @@
 
     @Test
     public void shouldIgnoreVibration_receivesNonWirelessChargingIntent_allowsAnyUsage() {
-        Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
-        setBatteryReceiverRegistrationResult(wirelessChargingIntent);
         setIgnoreVibrationsOnWirelessCharger(true);
         createSystemReadyVibrationSettings();
+
+        Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
+        mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+                mContextSpy, wirelessChargingIntent);
+
         // Check that initially, all usages are ignored due to the wireless charging.
         for (int usage : ALL_USAGES) {
             assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER);
         }
 
         Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
-        mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, nonWirelessChargingIntent);
+        mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+                mContextSpy, nonWirelessChargingIntent);
 
         for (int usage : ALL_USAGES) {
             assertVibrationNotIgnoredForUsage(usage);
@@ -400,7 +386,7 @@
                 USAGE_HARDWARE_FEEDBACK
         ));
 
-        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
 
         for (int usage : ALL_USAGES) {
             if (expectedAllowedVibrations.contains(usage)) {
@@ -413,7 +399,7 @@
 
     @Test
     public void shouldIgnoreVibration_notInBatterySaverMode_allowsAnyUsage() {
-        mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+        mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
 
         for (int usage : ALL_USAGES) {
             assertVibrationNotIgnoredForUsage(usage);
@@ -596,7 +582,7 @@
 
         // Testing the broadcast flow manually.
         when(mAudioManagerMock.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+        mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
                 new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
 
         assertVibrationIgnoredForUsage(USAGE_RINGTONE, Status.IGNORED_FOR_RINGER_MODE);
@@ -852,16 +838,15 @@
                 mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
 
         // Test early update of settings based on new user id.
-        putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW,
-                NEW_USER_ID);
-        mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID);
+        putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW, USER_ID);
+        mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID);
         assertEquals(VIBRATION_INTENSITY_LOW,
                 mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
 
         // Test later update of settings for UserHandle.USER_CURRENT.
         putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW,
                 UserHandle.USER_CURRENT);
-        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
+        mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
         assertEquals(VIBRATION_INTENSITY_LOW,
                 mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
     }
@@ -1009,7 +994,7 @@
     private void setRingerMode(int ringerMode) {
         when(mAudioManagerMock.getRingerModeInternal()).thenReturn(ringerMode);
         // Mock AudioManager broadcast of internal ringer mode change.
-        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+        mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
                 new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
     }
 
@@ -1024,14 +1009,6 @@
         return new CallerInfo(attrs, uid, VIRTUAL_DEVICE_ID, opPkg, null);
     }
 
-    private void setBatteryReceiverRegistrationResult(Intent result) {
-        doAnswer(invocation -> {
-            mRegisteredBatteryBroadcastReceiver = invocation.getArgument(0);
-            return result;
-        }).when(mContextSpy).registerReceiver(any(BroadcastReceiver.class),
-                argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
-    }
-
     private Intent getBatteryChangedIntent(int extraPluggedValue) {
         Intent batteryIntent = new Intent(Intent.ACTION_BATTERY_CHANGED);
         batteryIntent.putExtra(EXTRA_PLUGGED, extraPluggedValue);
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 31cc50f1..e83a4b2 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -29,13 +29,11 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -96,8 +94,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
@@ -187,11 +183,11 @@
         mVibratorProviders.clear();
         CombinedVibration effect = CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
     }
 
     @Test
@@ -200,11 +196,11 @@
                 .addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+        verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
     }
 
     @Test
@@ -212,34 +208,34 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
     @Test
     public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude() {
         VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -249,17 +245,17 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2, 3),
                 mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
@@ -277,13 +273,13 @@
         mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
         CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.completedFuture(
                 null);
-        long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
+        HalVibration vibration = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
                 USAGE_RINGTONE);
         waitForCompletion();
 
         verify(mStatsLoggerMock, never()).logVibrationParamRequestTimeout(UID);
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         for (int i = 0; i < amplitudes.size(); i++) {
             assertTrue(amplitudes.get(i) < 1 / 255f);
@@ -301,12 +297,13 @@
                 new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
 
         CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>();
-        long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE);
+        HalVibration vibration = startThreadAndDispatcher(effect, neverCompletingFuture,
+                USAGE_RINGTONE);
         waitForCompletion();
 
         verify(mStatsLoggerMock).logVibrationParamRequestTimeout(UID);
         assertEquals(Arrays.asList(expectedOneShot(15)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 1, 1),
                 mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
@@ -319,13 +316,13 @@
 
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(
                 waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length,
                         TEST_TIMEOUT_MILLIS));
         // Vibration still running after 2 cycles.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
         assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
 
         Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
@@ -333,15 +330,15 @@
                         /* uid= */ 1, /* deviceId= */ -1, /* opPkg= */ null, /* reason= */ null));
         mVibrationConductor.notifyCancelled(cancelVibrationInfo, /* immediate= */ false);
         waitForCompletion();
-        assertFalse(mThread.isRunningVibrationId(vibrationId));
+        assertFalse(mThread.isRunningVibrationId(vibration.id));
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verifyCallbacksTriggered(vibrationId, cancelVibrationInfo);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_SUPERSEDED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         List<Float> playedAmplitudes = fakeVibrator.getAmplitudes();
-        assertFalse(fakeVibrator.getEffectSegments(vibrationId).isEmpty());
+        assertFalse(fakeVibrator.getEffectSegments(vibration.id).isEmpty());
         assertFalse(playedAmplitudes.isEmpty());
 
         for (int i = 0; i < playedAmplitudes.size(); i++) {
@@ -358,17 +355,17 @@
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{1, 10, 100}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(expectedOneShot(5000)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @Test
@@ -377,16 +374,16 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
                 .isEqualTo(expectedOneShots(100L, 150L));
     }
 
@@ -398,16 +395,16 @@
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes,
                 /* repeat= */ -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
                 .isEqualTo(expectedOneShots(200L, 50L));
     }
 
@@ -420,7 +417,7 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         // We are expect this test to repeat the vibration effect twice, which would result in 5
         // segments being played:
         // 200ms ON
@@ -428,16 +425,17 @@
         // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
         // 150ms ON (100ms + 50ms, skips 0ms)
         // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
-        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5,
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() >= 5,
                 5000L + TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5))
+        assertThat(
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).subList(0, 5))
                 .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L));
     }
 
@@ -458,18 +456,18 @@
         VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                 .repeatEffectIndefinitely(effect)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(repeatingEffect);
+        HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
         // PWLE size max was used to generate a single vibrate call with 10 segments.
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -487,18 +485,18 @@
         VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                 .repeatEffectIndefinitely(effect)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(repeatingEffect);
+        HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false);
         waitForCompletion();
 
         // Composition size max was used to generate a single vibrate call with 10 primitives.
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+        assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -510,17 +508,17 @@
         int[] amplitudes = new int[]{1, 2, 3};
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5000, 500, 50}, amplitudes, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(expectedOneShot(5550)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @LargeTest
@@ -534,17 +532,17 @@
         VibrationEffect effect = VibrationEffect.createWaveform(
                 /* timings= */ new long[]{expectedOnDuration - 100, 50},
                 /* amplitudes= */ new int[]{1, 2}, /* repeat= */ 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
-        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1,
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() > 1,
                 expectedOnDuration + TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
-        List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibrationId);
+        List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibration.id);
         // First time, turn vibrator ON for the expected fixed duration.
         assertEquals(expectedOnDuration, effectSegments.get(0).getDuration());
         // Vibrator turns off in the middle of the second execution of the first step. Expect it to
@@ -567,11 +565,11 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -584,7 +582,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -597,11 +595,11 @@
         mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -614,7 +612,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -624,11 +622,11 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{100}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -641,7 +639,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -650,17 +648,17 @@
         mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -671,31 +669,31 @@
         HalVibration vibration = createVibration(CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK)));
         vibration.addFallback(VibrationEffect.EFFECT_CLICK, fallback);
-        long vibrationId = startThreadAndDispatcher(vibration);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
     @Test
     public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration() {
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
         verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
-        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
     }
 
     @Test
@@ -704,17 +702,17 @@
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID),
                 eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
                 .containsExactly(effect)
                 .inOrder();
     }
@@ -730,18 +728,18 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0),
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
     }
 
     @Test
@@ -749,14 +747,14 @@
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
         verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
-        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+        assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
     }
 
     @Test
@@ -774,13 +772,13 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         // Vibrator compose called twice.
-        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        assertEquals(3, fakeVibrator.getEffectSegments(vibrationId).size());
+        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        assertEquals(3, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -810,14 +808,14 @@
                         .build())
                 .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedOneShot(10),
@@ -829,7 +827,7 @@
                 expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f,
                         /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20),
                 expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
     }
 
@@ -851,18 +849,18 @@
                 .compose();
         HalVibration vibration = createVibration(CombinedVibration.createParallel(effect));
         vibration.addFallback(VibrationEffect.EFFECT_TICK, fallback);
-        long vibrationId = startThreadAndDispatcher(vibration);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
         verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         List<VibrationEffectSegment> segments =
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId);
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id);
         assertTrue("Wrong segments: " + segments, segments.size() >= 4);
         assertTrue(segments.get(0) instanceof PrebakedSegment);
         assertTrue(segments.get(1) instanceof PrimitiveSegment);
@@ -891,13 +889,13 @@
                 .addSustain(Duration.ofMillis(30))
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
         assertEquals(Arrays.asList(
                 expectedRamp(/* amplitude= */ 1, /* frequencyHz= */ 150, /* duration= */ 10),
@@ -907,8 +905,8 @@
                 expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.6f,
                         /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200,
                         /* duration= */ 40)),
-                fakeVibrator.getEffectSegments(vibrationId));
-        assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
+        assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibration.id));
     }
 
     @Test
@@ -932,15 +930,15 @@
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200))
                 .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments.
         // Using best split points instead of max-packing PWLEs.
-        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size());
+        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        assertEquals(6, fakeVibrator.getEffectSegments(vibration.id).size());
     }
 
     @Test
@@ -949,28 +947,28 @@
         fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2, TEST_TIMEOUT_MILLIS));
         // Vibration still running after 2 cycles.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
         assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
 
         mVibrationConductor.binderDied();
         waitForCompletion();
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
     }
 
     @Test
     public void vibrate_singleVibrator_skipsSyncedCallbacks() {
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
-        long vibrationId = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
+        HalVibration vibration = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         verify(mManagerHooks, never()).prepareSyncedVibration(anyLong(), any());
         verify(mManagerHooks, never()).triggerSyncedVibration(anyLong());
         verify(mManagerHooks, never()).cancelSyncedVibration();
@@ -984,18 +982,18 @@
                 .addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1007,26 +1005,26 @@
 
         CombinedVibration effect = CombinedVibration.createParallel(
                 VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         VibrationEffectSegment expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK);
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1049,32 +1047,32 @@
                         new long[]{10, 10}, new int[]{1, 2}, -1))
                 .addVibrator(4, composed)
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(4), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
         assertFalse(mControllers.get(4).isVibrating());
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(2).getAmplitudes());
         assertEquals(Arrays.asList(expectedOneShot(20)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2), mVibratorProviders.get(3).getAmplitudes());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(4).getEffectSegments(vibrationId));
+                mVibratorProviders.get(4).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1094,13 +1092,13 @@
                 .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50)
                 .addNext(2, composed, /* delay= */ 50)
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         waitForCompletion();
         InOrder controllerVerifier = inOrder(mControllerCallbacks);
-        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
 
         InOrder batteryVerifier = inOrder(mManagerHooks);
         batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
@@ -1110,19 +1108,19 @@
         batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
         batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
 
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(Arrays.asList(
                 expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1143,30 +1141,29 @@
         CombinedVibration effect = CombinedVibration.createParallel(composed);
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+        startThreadAndDispatcher(vibration);
 
         assertTrue(waitUntil(
-                () -> !mVibratorProviders.get(1).getEffectSegments(vibrationId).isEmpty()
-                        && !mVibratorProviders.get(2).getEffectSegments(vibrationId).isEmpty(),
+                () -> !mVibratorProviders.get(1).getEffectSegments(vibration.id).isEmpty()
+                        && !mVibratorProviders.get(2).getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifySyncedVibrationComplete();
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         VibrationEffectSegment expected = expectedPrimitive(
                 VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100);
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expected),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
     }
 
     @Test
@@ -1190,10 +1187,9 @@
                 .combine();
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC
@@ -1204,9 +1200,9 @@
                 | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM
                 | IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
     }
 
     @Test
@@ -1221,19 +1217,19 @@
                 .addVibrator(1, VibrationEffect.createOneShot(10, 100))
                 .addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks, never()).cancelSyncedVibration();
 
         assertEquals(Arrays.asList(expectedOneShot(10)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(Arrays.asList(expectedOneShot(5)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(200), mVibratorProviders.get(2).getAmplitudes());
     }
 
@@ -1250,10 +1246,9 @@
                 .combine();
         // We create the HalVibration here to obtain the vibration id and use it to mock the
         // required response when calling triggerSyncedVibration.
-        HalVibration halVibration = createVibration(effect);
-        long vibrationId = halVibration.id;
-        when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(false);
-        startThreadAndDispatcher(halVibration);
+        HalVibration vibration = createVibration(effect);
+        when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(false);
+        startThreadAndDispatcher(vibration);
         waitForCompletion();
 
         long expectedCap = IVibratorManager.CAP_SYNC
@@ -1262,7 +1257,7 @@
                 | IVibratorManager.CAP_MIXED_TRIGGER_ON
                 | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
         verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
-        verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+        verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
         verify(mManagerHooks).cancelSyncedVibration();
         assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty());
     }
@@ -1282,7 +1277,7 @@
                 .addVibrator(3, VibrationEffect.createWaveform(
                         new long[]{60}, new int[]{6}, -1))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         // All vibrators are turned on in parallel.
         assertTrue(waitUntil(
@@ -1295,20 +1290,20 @@
 
         verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L));
         verify(mManagerHooks).noteVibratorOff(eq(UID));
-        verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
-        verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+        verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
         assertFalse(mControllers.get(3).isVibrating());
 
         assertEquals(Arrays.asList(expectedOneShot(25)),
-                mVibratorProviders.get(1).getEffectSegments(vibrationId));
+                mVibratorProviders.get(1).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(80)),
-                mVibratorProviders.get(2).getEffectSegments(vibrationId));
+                mVibratorProviders.get(2).getEffectSegments(vibration.id));
         assertEquals(Arrays.asList(expectedOneShot(60)),
-                mVibratorProviders.get(3).getEffectSegments(vibrationId));
+                mVibratorProviders.get(3).getEffectSegments(vibration.id));
         assertEquals(expectedAmplitudes(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
         assertEquals(expectedAmplitudes(4, 5), mVibratorProviders.get(2).getAmplitudes());
         assertEquals(expectedAmplitudes(6), mVibratorProviders.get(3).getAmplitudes());
@@ -1327,17 +1322,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(expectedDuration + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(rampDownDuration + TEST_TIMEOUT_MILLIS);
@@ -1363,17 +1352,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(callbackDelay + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(TEST_TIMEOUT_MILLIS);
@@ -1397,17 +1380,11 @@
                 CombinedVibration.createParallel(
                         VibrationEffect.createOneShot(
                                 expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
-        CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
-        doAnswer(unused -> {
-            vibrationCompleteLatch.countDown();
-            return null;
-        }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
 
         startThreadAndDispatcher(vibration);
         long startTime = SystemClock.elapsedRealtime();
 
-        assertTrue(vibrationCompleteLatch.await(callbackTimeout + TEST_TIMEOUT_MILLIS,
-                TimeUnit.MILLISECONDS));
+        vibration.waitForEnd();
         long vibrationEndTime = SystemClock.elapsedRealtime();
 
         waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS);
@@ -1461,11 +1438,11 @@
         fakeVibrator.setOnLatency(latency);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
-        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(cancellingThread).
@@ -1480,7 +1457,7 @@
 
         // After the vibrator call ends the vibration is cancelled and the vibrator is turned off.
         waitForCompletion(/* timeout= */ latency + TEST_TIMEOUT_MILLIS);
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -1500,10 +1477,10 @@
                         .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                         .compose())
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1516,7 +1493,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1534,10 +1511,10 @@
                 .addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData()))
                 .addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData()))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1550,7 +1527,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1566,12 +1543,12 @@
                         new long[]{100, 100}, new int[]{1, 2}, 0))
                 .addVibrator(2, VibrationEffect.createOneShot(100, 100))
                 .combine();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(1).isVibrating()
                         && mControllers.get(2).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
         // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1584,7 +1561,7 @@
         waitForCompletion(/* timeout= */ 50);
         cancellingThread.join();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
         assertFalse(mControllers.get(1).isVibrating());
         assertFalse(mControllers.get(2).isVibrating());
     }
@@ -1592,19 +1569,17 @@
     @Test
     public void vibrate_binderDied_cancelsVibration() throws Exception {
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         mVibrationConductor.binderDied();
         waitForCompletion();
 
-        verify(mVibrationToken).linkToDeath(same(mVibrationConductor), eq(0));
-        verify(mVibrationToken).unlinkToDeath(same(mVibrationConductor), eq(0));
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
-        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
+        assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
     }
 
@@ -1615,15 +1590,15 @@
 
         VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{5, 5, 5}, new int[]{60, 120, 240}, -1);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         // Duration extended for 5 + 5 + 5 + 15.
         assertEquals(Arrays.asList(expectedOneShot(30)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         assertTrue(amplitudes.size() > 3);
         assertEquals(expectedAmplitudes(60, 120, 240), amplitudes.subList(0, 3));
@@ -1633,24 +1608,24 @@
     }
 
     @Test
-    public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() {
+    public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds()
+            throws Exception {
         when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(10_000);
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10, 200);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
 
         // Vibration completed but vibrator not yet released.
-        verify(mManagerHooks, timeout(TEST_TIMEOUT_MILLIS)).onVibrationCompleted(eq(vibrationId),
-                eq(new Vibration.EndInfo(Status.FINISHED)));
+        vibration.waitForEnd();
         verify(mManagerHooks, never()).onVibrationThreadReleased(anyLong());
 
         // Thread still running ramp down.
-        assertTrue(mThread.isRunningVibrationId(vibrationId));
+        assertTrue(mThread.isRunningVibrationId(vibration.id));
 
         // Duration extended for 10 + 10000.
         assertEquals(Arrays.asList(expectedOneShot(10_010)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
 
         // Will stop the ramp down right away.
         mVibrationConductor.notifyCancelled(
@@ -1658,9 +1633,8 @@
         waitForCompletion();
 
         // Does not cancel already finished vibration, but releases vibrator.
-        verify(mManagerHooks, never()).onVibrationCompleted(eq(vibrationId),
-                eq(new Vibration.EndInfo(Status.CANCELLED_BY_SETTINGS_UPDATE)));
-        verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+        assertThat(vibration.getStatus()).isNotEqualTo(Status.CANCELLED_BY_SETTINGS_UPDATE);
+        verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
     }
 
     @Test
@@ -1670,18 +1644,18 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         VibrationEffect effect = VibrationEffect.createOneShot(10_000, 240);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
                 TEST_TIMEOUT_MILLIS));
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+        verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
 
         // Duration extended for 10000 + 15.
         assertEquals(Arrays.asList(expectedOneShot(10_015)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
         assertTrue(amplitudes.size() > 1);
         for (int i = 1; i < amplitudes.size(); i++) {
@@ -1696,14 +1670,14 @@
         mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
 
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -1714,13 +1688,13 @@
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
 
         VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
-        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
                 .containsExactly(effect)
                 .inOrder();
         assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty();
@@ -1737,15 +1711,15 @@
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
                 .compose();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(
                 Arrays.asList(expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
     }
 
@@ -1764,14 +1738,14 @@
         VibrationEffect effect = VibrationEffect.startWaveform()
                 .addTransition(Duration.ofMillis(1), targetAmplitude(1))
                 .build();
-        long vibrationId = startThreadAndDispatcher(effect);
+        HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
 
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
-        verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedRamp(0, 1, 150, 150, 1)),
-                fakeVibrator.getEffectSegments(vibrationId));
+                fakeVibrator.getEffectSegments(vibration.id));
         assertTrue(fakeVibrator.getAmplitudes().isEmpty());
     }
 
@@ -1796,69 +1770,68 @@
         VibrationEffect effect4 = VibrationEffect.createOneShot(8000, 100);
         VibrationEffect effect5 = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
 
-        long vibrationId1 = startThreadAndDispatcher(effect1);
+        HalVibration vibration1 = startThreadAndDispatcher(effect1);
         waitForCompletion();
-        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
-        verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
 
-        long vibrationId2 = startThreadAndDispatcher(effect2);
+        HalVibration vibration2 = startThreadAndDispatcher(effect2);
         // Effect2 won't complete on its own. Cancel it after a couple of repeats.
         Thread.sleep(150);  // More than two TICKs.
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
         waitForCompletion();
 
-        long vibrationId3 = startThreadAndDispatcher(effect3);
+        HalVibration vibration3 = startThreadAndDispatcher(effect3);
         waitForCompletion();
 
         // Effect4 is a long oneshot, but it gets cancelled as fast as possible.
         long start4 = System.currentTimeMillis();
-        long vibrationId4 = startThreadAndDispatcher(effect4);
+        HalVibration vibration4 = startThreadAndDispatcher(effect4);
         mVibrationConductor.notifyCancelled(
                 new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ true);
         waitForCompletion();
         long duration4 = System.currentTimeMillis() - start4;
 
         // Effect5 is to show that things keep going after the immediate cancel.
-        long vibrationId5 = startThreadAndDispatcher(effect5);
+        HalVibration vibration5 = startThreadAndDispatcher(effect5);
         waitForCompletion();
 
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
 
         // Effect1
-        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
-        verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration1.id);
+        verifyCallbacksTriggered(vibration1, Status.FINISHED);
 
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                fakeVibrator.getEffectSegments(vibrationId1));
+                fakeVibrator.getEffectSegments(vibration1.id));
 
         // Effect2: repeating, cancelled.
-        verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibrationId2);
-        verifyCallbacksTriggered(vibrationId2, Status.CANCELLED_BY_USER);
+        verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibration2.id);
+        verifyCallbacksTriggered(vibration2, Status.CANCELLED_BY_USER);
 
         // The exact count of segments might vary, so just check that there's more than 2 and
         // all elements are the same segment.
-        List<VibrationEffectSegment> actualSegments2 = fakeVibrator.getEffectSegments(vibrationId2);
+        List<VibrationEffectSegment> actualSegments2 =
+                fakeVibrator.getEffectSegments(vibration2.id);
         assertTrue(actualSegments2.size() + " > 2", actualSegments2.size() > 2);
         for (VibrationEffectSegment segment : actualSegments2) {
             assertEquals(expectedPrebaked(VibrationEffect.EFFECT_TICK), segment);
         }
 
         // Effect3
-        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId3));
-        verifyCallbacksTriggered(vibrationId3, Status.FINISHED);
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id));
+        verifyCallbacksTriggered(vibration3, Status.FINISHED);
         assertEquals(Arrays.asList(
                         expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
-                fakeVibrator.getEffectSegments(vibrationId3));
+                fakeVibrator.getEffectSegments(vibration3.id));
 
         // Effect4: cancelled quickly.
-        verifyCallbacksTriggered(vibrationId4, Status.CANCELLED_BY_SCREEN_OFF);
+        verifyCallbacksTriggered(vibration4, Status.CANCELLED_BY_SCREEN_OFF);
         assertTrue("Tested duration=" + duration4, duration4 < 2000);
 
         // Effect5: played normally after effect4, which may or may not have played.
         assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
-                fakeVibrator.getEffectSegments(vibrationId5));
+                fakeVibrator.getEffectSegments(vibration5.id));
     }
 
     private void mockVibrators(int... vibratorIds) {
@@ -1875,19 +1848,19 @@
         mVibrationSettings.mSettingObserver.onChange(false);
     }
 
-    private long startThreadAndDispatcher(VibrationEffect effect) {
+    private HalVibration startThreadAndDispatcher(VibrationEffect effect) {
         return startThreadAndDispatcher(CombinedVibration.createParallel(effect));
     }
 
-    private long startThreadAndDispatcher(CombinedVibration effect) {
+    private HalVibration startThreadAndDispatcher(CombinedVibration effect) {
         return startThreadAndDispatcher(createVibration(effect));
     }
 
-    private long startThreadAndDispatcher(HalVibration vib) {
+    private HalVibration startThreadAndDispatcher(HalVibration vib) {
         return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null);
     }
 
-    private long startThreadAndDispatcher(VibrationEffect effect,
+    private HalVibration startThreadAndDispatcher(VibrationEffect effect,
             CompletableFuture<Void> requestVibrationParamsFuture, int usage) {
         VibrationAttributes attrs = new VibrationAttributes.Builder()
                 .setUsage(usage)
@@ -1898,14 +1871,14 @@
         return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
     }
 
-    private long startThreadAndDispatcher(HalVibration vib,
+    private HalVibration startThreadAndDispatcher(HalVibration vib,
             CompletableFuture<Void> requestVibrationParamsFuture) {
         mControllers = createVibratorControllers();
         DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers);
         mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter,
                 mVibrationScaler, mStatsLoggerMock, requestVibrationParamsFuture, mManagerHooks);
         assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor));
-        return mVibrationConductor.getVibration().id;
+        return mVibrationConductor.getVibration();
     }
 
     private boolean waitUntil(BooleanSupplier predicate, long timeout)
@@ -1994,13 +1967,9 @@
                 .collect(Collectors.toList());
     }
 
-    private void verifyCallbacksTriggered(long vibrationId, Status expectedStatus) {
-        verifyCallbacksTriggered(vibrationId, new Vibration.EndInfo(expectedStatus));
-    }
-
-    private void verifyCallbacksTriggered(long vibrationId, Vibration.EndInfo expectedEndInfo) {
-        verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedEndInfo));
-        verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+    private void verifyCallbacksTriggered(HalVibration vibration, Status expectedStatus) {
+        assertThat(vibration.getStatus()).isEqualTo(expectedStatus);
+        verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
     }
 
     private static final class TestLooperAutoDispatcher extends Thread {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
index 0d13be6..e8ca8bf 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -127,13 +127,13 @@
     public void setExternalControl_withCapability_enablesExternalControl() {
         mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
         VibratorController controller = createController();
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         controller.setExternalControl(true);
-        assertTrue(controller.isUnderExternalControl());
+        assertTrue(controller.isVibrating());
 
         controller.setExternalControl(false);
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
         inOrderVerifier.verify(mNativeWrapperMock).setExternalControl(eq(true));
@@ -143,10 +143,10 @@
     @Test
     public void setExternalControl_withNoCapability_ignoresExternalControl() {
         VibratorController controller = createController();
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         controller.setExternalControl(true);
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         verify(mNativeWrapperMock, never()).setExternalControl(anyBoolean());
     }
@@ -181,6 +181,38 @@
     }
 
     @Test
+    public void setAmplitude_vibratorIdle_ignoresAmplitude() {
+        VibratorController controller = createController();
+        assertFalse(controller.isVibrating());
+
+        controller.setAmplitude(1);
+        assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
+    public void setAmplitude_vibratorUnderExternalControl_ignoresAmplitude() {
+        mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorController controller = createController();
+        controller.setExternalControl(true);
+        assertTrue(controller.isVibrating());
+
+        controller.setAmplitude(1);
+        assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
+    public void setAmplitude_vibratorVibrating_setsAmplitude() {
+        when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
+        VibratorController controller = createController();
+        controller.on(100, /* vibrationId= */ 1);
+        assertTrue(controller.isVibrating());
+        assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0);
+
+        controller.setAmplitude(1);
+        assertEquals(1, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
     public void on_withDuration_turnsVibratorOn() {
         when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
         VibratorController controller = createController();
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 d99b20c6..538c3fc 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -266,12 +266,13 @@
     @After
     public void tearDown() throws Exception {
         if (mService != null) {
-            if (!mPendingVibrations.stream().allMatch(HalVibration::hasEnded)) {
-                // Cancel any pending vibration from tests.
-                cancelVibrate(mService);
-                for (HalVibration vibration : mPendingVibrations) {
-                    vibration.waitForEnd();
-                }
+            // Make sure we have permission to cancel test vibrations, even if the test denied them.
+            grantPermission(android.Manifest.permission.VIBRATE);
+            // Cancel any pending vibration from tests, including external vibrations.
+            cancelVibrate(mService);
+            // Wait until pending vibrations end asynchronously.
+            for (HalVibration vibration : mPendingVibrations) {
+                vibration.waitForEnd();
             }
             // Wait until all vibrators have stopped vibrating, waiting for ramp-down.
             // Note: if a test is flaky here something is wrong with the vibration finalization.
@@ -2242,7 +2243,7 @@
         VibratorManagerService service = createSystemReadyService();
 
         VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100);
-        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+        HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
 
         // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2255,7 +2256,8 @@
         assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
 
         // Vibration is cancelled.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        vibration.waitForEnd();
+        assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
         assertEquals(Arrays.asList(false, true),
                 mVibratorProviders.get(1).getExternalControlStates());
     }
@@ -2296,7 +2298,7 @@
 
         VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
                 new long[]{100, 200, 300}, new int[]{128, 255, 255}, 1);
-        vibrate(service, repeatingEffect, ALARM_ATTRS);
+        HalVibration repeatingVibration = vibrate(service, repeatingEffect, ALARM_ATTRS);
 
         // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2308,7 +2310,8 @@
         assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
 
         // Vibration is cancelled.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        repeatingVibration.waitForEnd();
+        assertThat(repeatingVibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
         assertEquals(Arrays.asList(false, true),
                 mVibratorProviders.get(1).getExternalControlStates());
     }
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
index 1c33116..6f9c890 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -22,6 +22,7 @@
 import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
 import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
 
+import android.platform.test.annotations.DisableFlags;
 import android.view.ViewConfiguration;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -39,6 +40,7 @@
  */
 @MediumTest
 @RunWith(AndroidJUnit4.class)
+@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES)
 public class CombinationKeyTests extends ShortcutKeyTestBase {
     private static final long A11Y_KEY_HOLD_MILLIS = 3500;
 
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
index 487390d..8b5f68a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
@@ -71,12 +71,12 @@
                 new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
                         KEYCODE_POWER) {
                     @Override
-                    void execute() {
+                    public void execute() {
                         mAction1Triggered.countDown();
                     }
 
                     @Override
-                    void cancel() {
+                    public void cancel() {
                     }
                 });
 
@@ -85,21 +85,21 @@
                 new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
                         KEYCODE_VOLUME_UP) {
                     @Override
-                    boolean preCondition() {
+                    public boolean preCondition() {
                         return mPreCondition;
                     }
 
                     @Override
-                    void execute() {
+                    public void execute() {
                         mAction2Triggered.countDown();
                     }
 
                     @Override
-                    void cancel() {
+                    public void cancel() {
                     }
 
                     @Override
-                    long getKeyInterceptDelayMs() {
+                    public long getKeyInterceptDelayMs() {
                         return 0;
                     }
                 });
@@ -115,12 +115,12 @@
                     };
 
                     @Override
-                    void execute() {
+                    public void execute() {
                         mHandler.postDelayed(mAction, SCHEDULE_TIME);
                     }
 
                     @Override
-                    void cancel() {
+                    public void cancel() {
                         mHandler.removeCallbacks(mAction);
                     }
                 });
@@ -235,12 +235,12 @@
                 new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
                         KEYCODE_POWER) {
                     @Override
-                    void execute() {
+                    public void execute() {
                         mAction1Triggered.countDown();
                     }
 
                     @Override
-                    void cancel() {
+                    public void cancel() {
                     }
                 };
 
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3d978e4..cdb4542 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -22,10 +22,13 @@
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ALL_APPS;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ASSIST;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_NOTIFICATION_PANEL;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
 import static com.android.server.policy.PhoneWindowManager.SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL;
 
 import android.hardware.input.KeyGestureEvent;
 import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.KeyEvent;
@@ -56,88 +59,15 @@
     private static final int CTRL_ON = MODIFIER.get(KeyEvent.KEYCODE_CTRL_LEFT);
 
     @Keep
-    private static Object[][] shortcutTestArguments() {
+    private static Object[][] shortcutTestArgumentsNotMigratedToKeyGestureController() {
         // testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
         return new Object[][]{
-                {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
-                {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
-                        META_ON},
                 {"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME},
                         KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
                         KeyEvent.KEYCODE_HOME, 0},
-                {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
-                        KeyEvent.KEYCODE_RECENT_APPS, 0},
-                {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
-                        META_ON},
-                {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
-                        ALT_ON},
                 {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK},
                         KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
                         KeyEvent.KEYCODE_BACK, 0},
-                {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
-                        META_ON},
-                {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
-                        META_ON},
-                {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
-                {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
-                        KeyEvent.KEYCODE_APP_SWITCH, 0},
-                {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
-                        KeyEvent.KEYCODE_ASSIST, 0},
-                {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
-                        META_ON},
-                {"VOICE_ASSIST key -> Launch Voice Assistant",
-                        new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
-                        KeyEvent.KEYCODE_VOICE_ASSIST, 0},
-                {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
-                        KeyEvent.KEYCODE_I, META_ON},
-                {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
-                        KeyEvent.KEYCODE_N, META_ON},
-                {"NOTIFICATION key -> Toggle Notification Panel",
-                        new int[]{KeyEvent.KEYCODE_NOTIFICATION},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
-                        KeyEvent.KEYCODE_NOTIFICATION,
-                        0},
-                {"Meta + Ctrl + S -> Take Screenshot",
-                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
-                        META_ON | CTRL_ON},
-                {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
-                        KeyEvent.KEYCODE_SLASH, META_ON},
-                {"BRIGHTNESS_UP key -> Increase Brightness",
-                        new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
-                        KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
-                {"BRIGHTNESS_DOWN key -> Decrease Brightness",
-                        new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
-                        KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
-                {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
-                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
-                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
-                {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
-                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
-                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
-                {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
-                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
-                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
                 {"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP},
                         KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_UP,
                         KeyEvent.KEYCODE_VOLUME_UP, 0},
@@ -147,53 +77,9 @@
                 {"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE},
                         KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_MUTE,
                         KeyEvent.KEYCODE_VOLUME_MUTE, 0},
-                {"ALL_APPS key -> Open App Drawer",
-                        new int[]{KeyEvent.KEYCODE_ALL_APPS},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
-                        KeyEvent.KEYCODE_ALL_APPS, 0},
-                {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
-                        KeyEvent.KEYCODE_SEARCH, 0},
-                {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
-                        new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
-                        KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
-                {"META key -> Open App Drawer", new int[]{META_KEY},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
-                        META_ON},
-                {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
-                        META_ON | ALT_ON},
-                {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
-                        META_ON | ALT_ON},
-                {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
-                        KeyEvent.KEYCODE_CAPS_LOCK, 0},
                 {"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE},
                         KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE,
                         0},
-                {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
-                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
-                        KeyEvent.KEYCODE_DPAD_UP,
-                        META_ON | CTRL_ON},
-                {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
-                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
-                        KeyEvent.KEYCODE_DPAD_LEFT,
-                        META_ON | CTRL_ON},
-                {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
-                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
-                        KeyEvent.KEYCODE_DPAD_RIGHT,
-                        META_ON | CTRL_ON},
-                {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
-                        META_ON},
-                {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
-                        KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
-                        META_ON | CTRL_ON},
                 {"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER},
                         KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, KeyEvent.KEYCODE_POWER,
                         0},
@@ -279,7 +165,130 @@
                 {"Meta + S -> Launch Default Messaging App",
                         new int[]{META_KEY, KeyEvent.KEYCODE_S},
                         KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING,
-                        KeyEvent.KEYCODE_S, META_ON},
+                        KeyEvent.KEYCODE_S, META_ON}};
+    }
+
+    @Keep
+    private static Object[][] shortcutTestArgumentsMigratedToKeyGestureController() {
+        // testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
+        return new Object[][]{
+                {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
+                {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
+                        META_ON},
+                {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+                        KeyEvent.KEYCODE_RECENT_APPS, 0},
+                {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+                        META_ON},
+                {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+                        ALT_ON},
+                {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
+                        META_ON},
+                {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
+                        META_ON},
+                {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
+                {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+                        KeyEvent.KEYCODE_APP_SWITCH, 0},
+                {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+                        KeyEvent.KEYCODE_ASSIST, 0},
+                {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
+                        META_ON},
+                {"VOICE_ASSIST key -> Launch Voice Assistant",
+                        new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
+                        KeyEvent.KEYCODE_VOICE_ASSIST, 0},
+                {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+                        KeyEvent.KEYCODE_I, META_ON},
+                {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+                        KeyEvent.KEYCODE_N, META_ON},
+                {"NOTIFICATION key -> Toggle Notification Panel",
+                        new int[]{KeyEvent.KEYCODE_NOTIFICATION},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+                        KeyEvent.KEYCODE_NOTIFICATION,
+                        0},
+                {"Meta + Ctrl + S -> Take Screenshot",
+                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
+                        META_ON | CTRL_ON},
+                {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+                        KeyEvent.KEYCODE_SLASH, META_ON},
+                {"BRIGHTNESS_UP key -> Increase Brightness",
+                        new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
+                        KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
+                {"BRIGHTNESS_DOWN key -> Decrease Brightness",
+                        new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
+                        KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
+                {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
+                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
+                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
+                {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
+                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
+                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
+                {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
+                        new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
+                        KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
+                {"ALL_APPS key -> Open App Drawer",
+                        new int[]{KeyEvent.KEYCODE_ALL_APPS},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
+                        KeyEvent.KEYCODE_ALL_APPS, 0},
+                {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+                        KeyEvent.KEYCODE_SEARCH, 0},
+                {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
+                        new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                        KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
+                {"META key -> Open App Drawer", new int[]{META_KEY},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
+                        META_ON},
+                {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
+                        META_ON | ALT_ON},
+                {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
+                        META_ON | ALT_ON},
+                {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+                        KeyEvent.KEYCODE_CAPS_LOCK, 0},
+                {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
+                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
+                        KeyEvent.KEYCODE_DPAD_UP,
+                        META_ON | CTRL_ON},
+                {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
+                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
+                        KeyEvent.KEYCODE_DPAD_LEFT,
+                        META_ON | CTRL_ON},
+                {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
+                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
+                        KeyEvent.KEYCODE_DPAD_RIGHT,
+                        META_ON | CTRL_ON},
+                {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
+                        META_ON},
+                {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
+                        KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
+                        META_ON | CTRL_ON},
                 {"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode",
                         new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN},
                         KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE,
@@ -296,72 +305,14 @@
                         new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
                         KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
                         KeyEvent.KEYCODE_HOME, 0},
-                {"Long press META + ENTER -> Toggle Notification panel",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
-                        LONG_PRESS_HOME_NOTIFICATION_PANEL,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
-                        KeyEvent.KEYCODE_ENTER,
-                        META_ON},
-                {"Long press META + H -> Toggle Notification panel",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
-                        KeyEvent.KEYCODE_H, META_ON},
                 {"Long press HOME key -> Launch assistant",
                         new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ASSIST,
                         KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
                         KeyEvent.KEYCODE_HOME, 0},
-                {"Long press META + ENTER -> Launch assistant",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ASSIST,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
-                        KeyEvent.KEYCODE_ENTER, META_ON},
-                {"Long press META + H -> Launch assistant",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_ASSIST,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_H,
-                        META_ON},
                 {"Long press HOME key -> Open App Drawer",
                         new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ALL_APPS,
                         KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
-                        KeyEvent.KEYCODE_HOME, 0},
-                {"Long press META + ENTER -> Open App Drawer",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ALL_APPS,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
-                        KeyEvent.KEYCODE_ENTER, META_ON},
-                {"Long press META + H -> Open App Drawer",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_H},
-                        LONG_PRESS_HOME_ALL_APPS,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
-                        KeyEvent.KEYCODE_H, META_ON}};
-    }
-
-    @Keep
-    private static Object[][] doubleTapOnHomeTestArguments() {
-        // testName, testKeys, doubleTapOnHomeBehavior, expectedKeyGestureType, expectedKey,
-        // expectedModifierState
-        return new Object[][]{
-                {"Double tap HOME -> Open App switcher",
-                        new int[]{KeyEvent.KEYCODE_HOME}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_HOME,
-                        0},
-                {"Double tap META + ENTER -> Open App switcher",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
-                        DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
-                        KeyEvent.KEYCODE_ENTER, META_ON},
-                {"Double tap META + H -> Open App switcher",
-                        new int[]{META_KEY, KeyEvent.KEYCODE_H}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_H,
-                        META_ON}};
-    }
-
-    @Keep
-    private static Object[][] settingsKeyTestArguments() {
-        // testName, testKeys, settingsKeyBehavior, expectedKeyGestureType, expectedKey,
-        // expectedModifierState
-        return new Object[][]{
-                {"SETTINGS key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_SETTINGS},
-                        SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
-                        KeyEvent.KEYCODE_SETTINGS, 0}};
+                        KeyEvent.KEYCODE_HOME, 0}};
     }
 
     @Before
@@ -381,8 +332,18 @@
     }
 
     @Test
-    @Parameters(method = "shortcutTestArguments")
-    public void testShortcut(String testName, int[] testKeys,
+    @Parameters(method = "shortcutTestArgumentsNotMigratedToKeyGestureController")
+    public void testShortcuts_notMigratedToKeyGestureController(String testName,
+            int[] testKeys, @KeyGestureEvent.KeyGestureType int expectedKeyGestureType,
+            int expectedKey, int expectedModifierState) {
+        testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
+                expectedModifierState);
+    }
+
+    @Test
+    @Parameters(method = "shortcutTestArgumentsMigratedToKeyGestureController")
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    public void testShortcuts_migratedToKeyGestureController(String testName, int[] testKeys,
             @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
             int expectedModifierState) {
         testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
@@ -402,31 +363,29 @@
     }
 
     @Test
-    @Parameters(method = "doubleTapOnHomeTestArguments")
-    public void testDoubleTapOnHomeBehavior(String testName, int[] testKeys,
-            int doubleTapOnHomeBehavior,
-            @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
-            int expectedModifierState) {
-        mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(doubleTapOnHomeBehavior);
-        sendKeyCombination(testKeys, 0 /* duration */);
-        sendKeyCombination(testKeys, 0 /* duration */);
+    public void testDoubleTapOnHomeBehavior_AppSwitchBehavior() {
+        mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(DOUBLE_TAP_HOME_RECENT_SYSTEM_UI);
+        sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
+        sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
         mPhoneWindowManager.assertKeyGestureCompleted(
-                new int[]{expectedKey}, expectedModifierState, expectedKeyGestureType,
-                "Failed while executing " + testName);
+                new int[]{KeyEvent.KEYCODE_HOME}, /* modifierState = */0,
+                KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+                "Failed while executing Double tap HOME -> Open App switcher");
     }
 
     @Test
-    @Parameters(method = "settingsKeyTestArguments")
-    public void testSettingsKey(String testName, int[] testKeys, int settingsKeyBehavior,
-            @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
-            int expectedModifierState) {
-        mPhoneWindowManager.overrideSettingsKeyBehavior(settingsKeyBehavior);
-        testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
-                expectedModifierState);
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    public void testSettingsKey_ToggleNotificationBehavior() {
+        mPhoneWindowManager.overrideSettingsKeyBehavior(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL);
+        testShortcutInternal("SETTINGS key -> Toggle Notification panel",
+                new int[]{KeyEvent.KEYCODE_SETTINGS},
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+                KeyEvent.KEYCODE_SETTINGS, 0);
     }
 
     @Test
     @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testBugreportShortcutPress() {
         testShortcutInternal("Meta + Ctrl + Del -> Trigger bug report",
                 new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DEL},
@@ -599,4 +558,145 @@
                 sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH));
         mPhoneWindowManager.assertLaunchSearch();
     }
+
+    @Test
+    public void testKeyGestureScreenshotChord() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        mPhoneWindowManager.moveTimeForward(500);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        mPhoneWindowManager.assertTakeScreenshotCalled();
+    }
+
+    @Test
+    public void testKeyGestureScreenshotChordCancelled() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        mPhoneWindowManager.assertTakeScreenshotNotCalled();
+    }
+
+    @Test
+    public void testKeyGestureAccessibilityShortcutChord() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.moveTimeForward(5000);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.assertAccessibilityKeychordCalled();
+    }
+
+    @Test
+    public void testKeyGestureAccessibilityShortcutChordCancelled() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+    }
+
+    @Test
+    public void testKeyGestureRingerToggleChord() {
+        mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        mPhoneWindowManager.moveTimeForward(500);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        mPhoneWindowManager.assertVolumeMute();
+    }
+
+    @Test
+    public void testKeyGestureRingerToggleChordCancelled() {
+        mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        mPhoneWindowManager.assertVolumeNotMuted();
+    }
+
+    @Test
+    public void testKeyGestureGlobalAction() {
+        mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        mPhoneWindowManager.moveTimeForward(500);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        mPhoneWindowManager.assertShowGlobalActionsCalled();
+    }
+
+    @Test
+    public void testKeyGestureGlobalActionCancelled() {
+        mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        mPhoneWindowManager.assertShowGlobalActionsNotCalled();
+    }
+
+    @Test
+    public void testKeyGestureAccessibilityTvShortcutChord() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.moveTimeForward(5000);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.assertAccessibilityKeychordCalled();
+    }
+
+    @Test
+    public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+        mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+    }
+
+    @Test
+    public void testKeyGestureTvTriggerBugReport() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        mPhoneWindowManager.moveTimeForward(1000);
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        mPhoneWindowManager.assertBugReportTakenForTv();
+    }
+
+    @Test
+    public void testKeyGestureTvTriggerBugReportCancelled() {
+        Assert.assertTrue(
+                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        Assert.assertTrue(
+                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        mPhoneWindowManager.assertBugReportNotTakenForTv();
+    }
+
+    @Test
+    public void testKeyGestureAccessibilityShortcut() {
+        Assert.assertTrue(
+                sendKeyGestureEventComplete(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT));
+        mPhoneWindowManager.assertAccessibilityKeychordCalled();
+    }
+
+    @Test
+    public void testKeyGestureCloseAllDialogs() {
+        Assert.assertTrue(
+                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS));
+        mPhoneWindowManager.assertCloseAllDialogs();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 43171f8..c186a03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -119,6 +119,7 @@
      * ALT + TAB to show recent apps.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testAltTab() {
         mPhoneWindowManager.overrideStatusBarManagerInternal();
         sendKeyCombination(new int[]{KEYCODE_ALT_LEFT, KEYCODE_TAB}, 0);
@@ -129,6 +130,7 @@
      * CTRL + SPACE to switch keyboard layout.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testCtrlSpace() {
         sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, /* duration= */ 0,
                 ANY_DISPLAY_ID);
@@ -139,6 +141,7 @@
      * CTRL + SHIFT + SPACE to switch keyboard layout backwards.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testCtrlShiftSpace() {
         sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SHIFT_LEFT, KEYCODE_SPACE},
                 /* duration= */ 0, ANY_DISPLAY_ID);
@@ -149,6 +152,7 @@
      * CTRL + ALT + Z to enable accessibility service.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testCtrlAltZ() {
         sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_ALT_LEFT, KEYCODE_Z}, 0);
         mPhoneWindowManager.assertAccessibilityKeychordCalled();
@@ -158,6 +162,7 @@
      * META + CTRL+ S to take screenshot.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaCtrlS() {
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_S}, 0);
         mPhoneWindowManager.assertTakeScreenshotCalled();
@@ -167,6 +172,7 @@
      * META + N to expand notification panel.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaN() throws RemoteException {
         mPhoneWindowManager.overrideTogglePanel();
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_N}, 0);
@@ -177,6 +183,7 @@
      * META + SLASH to toggle shortcuts menu.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaSlash() {
         mPhoneWindowManager.overrideStatusBarManagerInternal();
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SLASH}, 0);
@@ -187,6 +194,7 @@
      * META  + ALT to toggle Cap Lock.
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaAlt() {
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
         mPhoneWindowManager.assertToggleCapsLock();
@@ -196,6 +204,7 @@
      * META + H to go to homescreen
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaH() {
         mPhoneWindowManager.overrideLaunchHome();
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_H}, 0);
@@ -206,6 +215,7 @@
      * META + ENTER to go to homescreen
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testMetaEnter() {
         mPhoneWindowManager.overrideLaunchHome();
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0);
@@ -216,6 +226,7 @@
      * Sends a KEYCODE_BRIGHTNESS_DOWN event and validates the brightness is decreased as expected;
      */
     @Test
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testKeyCodeBrightnessDown() {
         float[] currentBrightness = new float[]{0.1f, 0.05f, 0.0f};
         float[] newBrightness = new float[]{0.065738f, 0.0275134f, 0.0f};
@@ -231,9 +242,9 @@
      * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
      */
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testTakeScreenshot_flagEnabled() {
-        mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
-                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
         sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
         mPhoneWindowManager.assertTakeScreenshotCalled();
     }
@@ -242,9 +253,9 @@
      * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
      */
     @Test
+    @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE,
+            com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
     public void testTakeScreenshot_flagDisabled() {
-        mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
-                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
         sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
         mPhoneWindowManager.assertTakeScreenshotNotCalled();
     }
@@ -254,6 +265,7 @@
      */
     @Test
     @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testTakeBugReport_flagEnabled() throws RemoteException {
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
         mPhoneWindowManager.assertTakeBugreport(true);
@@ -263,7 +275,8 @@
      * META+CTRL+BACKSPACE for taking a bugreport does nothing when the flag is disabledd.
      */
     @Test
-    @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+    @DisableFlags({com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
+            com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
     public void testTakeBugReport_flagDisabled() throws RemoteException {
         sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
         mPhoneWindowManager.assertTakeBugreport(false);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 50b7db4..9e47a00 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -241,6 +241,13 @@
                         KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
     }
 
+    boolean sendKeyGestureEventCancel(int gestureType) {
+        return mPhoneWindowManager.sendKeyGestureEvent(
+                new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
+                        KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags(
+                        KeyGestureEvent.FLAG_CANCELLED).build());
+    }
+
     boolean sendKeyGestureEventComplete(int gestureType, int modifierState) {
         return mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType(
@@ -276,7 +283,7 @@
         if ((actions & ACTION_PASS_TO_USER) != 0) {
             if (0 == mPhoneWindowManager.interceptKeyBeforeDispatching(keyEvent)) {
                 if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) {
-                    mPhoneWindowManager.dispatchUnhandledKey(keyEvent);
+                    mPhoneWindowManager.interceptUnhandledKey(keyEvent);
                 }
             }
         }
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 98401b3..1aa9087 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -418,8 +418,8 @@
                 mKeyEventPolicyFlags);
     }
 
-    void dispatchUnhandledKey(KeyEvent event) {
-        mPhoneWindowManager.dispatchUnhandledKey(mInputToken, event, FLAG_INTERACTIVE);
+    void interceptUnhandledKey(KeyEvent event) {
+        mPhoneWindowManager.interceptUnhandledKey(event, mInputToken);
     }
 
     boolean sendKeyGestureEvent(KeyGestureEvent event) {
@@ -657,16 +657,36 @@
         verify(mPowerManager).userActivity(anyLong(), anyBoolean());
     }
 
+    void assertShowGlobalActionsNotCalled() {
+        mTestLooper.dispatchAll();
+        verify(mGlobalActions, never()).showDialog(anyBoolean(), anyBoolean());
+        verify(mPowerManager, never()).userActivity(anyLong(), anyBoolean());
+    }
+
     void assertVolumeMute() {
         mTestLooper.dispatchAll();
         verify(mAudioManagerInternal).silenceRingerModeInternal(eq("volume_hush"));
     }
 
+    void assertVolumeNotMuted() {
+        mTestLooper.dispatchAll();
+        verify(mAudioManagerInternal, never()).silenceRingerModeInternal(any());
+    }
+
     void assertAccessibilityKeychordCalled() {
         mTestLooper.dispatchAll();
         verify(mAccessibilityShortcutController).performAccessibilityShortcut();
     }
 
+    void assertAccessibilityKeychordNotCalled() {
+        mTestLooper.dispatchAll();
+        verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
+    }
+
+    void assertCloseAllDialogs() {
+        verify(mContext).closeSystemDialogs();
+    }
+
     void assertDreamRequest() {
         mTestLooper.dispatchAll();
         verify(mDreamManagerInternal).requestDream();
@@ -809,6 +829,16 @@
 
     }
 
+    void assertBugReportTakenForTv() {
+        mTestLooper.dispatchAll();
+        verify(mPhoneWindowManager).requestBugreportForTv();
+    }
+
+    void assertBugReportNotTakenForTv() {
+        mTestLooper.dispatchAll();
+        verify(mPhoneWindowManager, never()).requestBugreportForTv();
+    }
+
     void assertTogglePanel() throws RemoteException {
         mTestLooper.dispatchAll();
         verify(mPhoneWindowManager.mStatusBarService).togglePanel();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8227ed9..92205f39 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -230,6 +230,10 @@
         mDisplayContent.setIgnoreOrientationRequest(enabled);
     }
 
+    void setTopActivityOrganizedTask() {
+        doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask();
+    }
+
     void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) {
         doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index d66c21a..b91a5b7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -28,7 +28,7 @@
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import android.compat.testing.PlatformCompatChangeRule;
 import android.platform.test.annotations.DisableFlags;
@@ -218,7 +218,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -229,7 +229,7 @@
 
     @Test
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -240,7 +240,7 @@
 
     @Test
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -250,7 +250,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index d91b38e..41102d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,7 +20,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -85,7 +85,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -95,7 +95,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -105,7 +105,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -115,7 +115,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -125,7 +125,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -136,7 +136,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_existsWhenCameraCompatFreeformExists() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(true);
@@ -147,7 +147,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_startedWhenCameraCompatFreeformExists() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(true);
@@ -180,7 +180,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_doesNotExistWhenNoPolicyExists() {
         runTestScenario((robot) -> {
             robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
index 21fac9b..d8373c5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
@@ -16,10 +16,14 @@
 
 package com.android.server.wm;
 
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
 import static org.mockito.Mockito.when;
 
+import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
+import android.app.TaskInfo;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
@@ -114,6 +118,72 @@
         });
     }
 
+    @Test
+    public void testTopActivityEligibleForUserAspectRatioButton_eligible() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponentInNewTask();
+                a.setIgnoreOrientationRequest(true);
+            });
+            robot.conf().enableUserAppAspectRatioSettings(true);
+
+            robot.checkTaskInfoEligibleForUserAspectRatioButton(true);
+        });
+    }
+
+    @Test
+    public void testTopActivityEligibleForUserAspectRatioButton_disabled_notEligible() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponentInNewTask();
+                a.setIgnoreOrientationRequest(true);
+            });
+            robot.conf().enableUserAppAspectRatioSettings(false);
+
+            robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+        });
+    }
+
+    @Test
+    public void testTopActivityEligibleForUserAspectRatioButton_inSizeCompatMode_notEligible() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponentInNewTask();
+                a.setIgnoreOrientationRequest(true);
+                a.setTopActivityOrganizedTask();
+                a.setTopActivityInSizeCompatMode(true);
+                a.setTopActivityVisible(true);
+            });
+            robot.conf().enableUserAppAspectRatioSettings(true);
+
+            robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+        });
+    }
+
+    @Test
+    public void testTopActivityEligibleForUserAspectRatioButton_transparentTop_notEligible() {
+        runTestScenario((robot) -> {
+            robot.transparentActivity((ta) -> {
+                ta.launchTransparentActivityInTask();
+                ta.activity().setIgnoreOrientationRequest(true);
+            });
+            robot.conf().enableUserAppAspectRatioSettings(true);
+
+            robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+        });
+    }
+
+    @Test
+    public void getTaskInfoPropagatesCameraCompatMode() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity(AppCompatActivityRobot::createActivityWithComponentInNewTask);
+
+            robot.setFreeformCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+            robot.checkTaskInfoFreeformCameraCompatMode(
+                    CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -125,11 +195,14 @@
     private static class AppCompatUtilsRobotTest extends AppCompatRobotBase {
 
         private final WindowState mWindowState;
+        @NonNull
+        private final AppCompatTransparentActivityRobot mTransparentActivityRobot;
 
         AppCompatUtilsRobotTest(@NonNull WindowManagerService wm,
                 @NonNull ActivityTaskManagerService atm,
                 @NonNull ActivityTaskSupervisor supervisor) {
             super(wm, atm, supervisor);
+            mTransparentActivityRobot = new AppCompatTransparentActivityRobot(activity());
             mWindowState = Mockito.mock(WindowState.class);
         }
 
@@ -139,6 +212,12 @@
             spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
         }
 
+        void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) {
+            // We always create at least an opaque activity in a Task.
+            activity().createNewTaskWithBaseActivity();
+            consumer.accept(mTransparentActivityRobot);
+        }
+
         void setIsLetterboxedForFixedOrientationAndAspectRatio(
                 boolean forFixedOrientationAndAspectRatio) {
             when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy()
@@ -155,11 +234,30 @@
             when(mWindowState.isLetterboxedForDisplayCutout()).thenReturn(displayCutout);
         }
 
+        void setFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+            activity().top().mAppCompatController.getAppCompatCameraOverrides()
+                    .setFreeformCameraCompatMode(mode);
+        }
+
         void checkTopActivityLetterboxReason(@NonNull String expected) {
             Assert.assertEquals(expected,
                     AppCompatUtils.getLetterboxReasonString(activity().top(), mWindowState));
         }
 
+        @NonNull
+        TaskInfo getTopTaskInfo() {
+            return activity().top().getTask().getTaskInfo();
+        }
+
+        void checkTaskInfoEligibleForUserAspectRatioButton(boolean eligible) {
+            Assert.assertEquals(eligible, getTopTaskInfo().appCompatTaskInfo
+                    .eligibleForUserAspectRatioButton());
+        }
+
+        void checkTaskInfoFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+            Assert.assertEquals(mode, getTopTaskInfo().appCompatTaskInfo
+                    .cameraCompatTaskInfo.freeformCameraCompatMode);
+        }
     }
 
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index a48813d..dbcef10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -35,7 +35,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -60,6 +60,7 @@
 import android.graphics.Rect;
 import android.hardware.camera2.CameraManager;
 import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.DisplayInfo;
 import android.view.Surface;
@@ -135,7 +136,6 @@
                 });
 
         mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler);
-        mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM);
         CameraStateMonitor cameraStateMonitor =
                 new CameraStateMonitor(mDisplayContent, mMockHandler);
         mCameraCompatFreeformPolicy =
@@ -147,6 +147,7 @@
         cameraStateMonitor.startListeningToCameraState();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testFullscreen_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
@@ -157,6 +158,7 @@
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
@@ -164,12 +166,14 @@
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -210,6 +214,7 @@
         assertActivityRefreshRequested(/* refreshRequested */ false);
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -235,6 +240,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -246,6 +252,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
@@ -254,6 +261,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -268,6 +276,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
         when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
@@ -281,6 +290,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 85cb1bc..5c0d424 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -83,7 +83,7 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -2820,7 +2820,7 @@
         verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
     }
 
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() {
         doReturn(true).when(() ->
@@ -2829,7 +2829,7 @@
         assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
         doReturn(true).when(() ->
@@ -2838,7 +2838,7 @@
         assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @Test
     public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index f32a234..6a89178 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -413,15 +413,25 @@
 
     @Test
     public void testUpdateDisplayConfigurationByDecor() {
-        if (Flags.insetsDecoupledConfiguration()) {
-            // No configuration update when flag enables.
-            return;
-        }
         doReturn(NO_CUTOUT).when(mDisplayContent).calculateDisplayCutoutForRotation(anyInt());
         final WindowState navbar = createNavBarWithProvidedInsets(mDisplayContent);
         final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
         final DisplayInfo di = mDisplayContent.getDisplayInfo();
         final int prevScreenHeightDp = mDisplayContent.getConfiguration().screenHeightDp;
+        if (Flags.insetsDecoupledConfiguration()) {
+            // No configuration update when flag enables.
+            assertFalse(displayPolicy.updateDecorInsetsInfo());
+            assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(di.rotation,
+                    di.logicalHeight, di.logicalWidth).mOverrideConfigInsets.bottom);
+
+            final int barHeight = 2 * NAV_BAR_HEIGHT;
+            navbar.mAttrs.providedInsets[0].setInsetsSize(Insets.of(0, 0, 0, barHeight));
+            assertFalse(displayPolicy.updateDecorInsetsInfo());
+            assertEquals(barHeight, displayPolicy.getDecorInsetsInfo(di.rotation,
+                    di.logicalHeight, di.logicalWidth).mOverrideConfigInsets.bottom);
+            return;
+        }
+
         assertTrue(navbar.providesDisplayDecorInsets() && displayPolicy.updateDecorInsetsInfo());
         assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(di.rotation,
                 di.logicalWidth, di.logicalHeight).mConfigInsets.bottom);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index e0344d7..df17cd1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -432,6 +432,29 @@
     }
 
     @Test
+    public void testAddTaskCompatibleWindowingMode_withFreeformAndFullscreen_expectRemove() {
+        Task task1 = createTaskBuilder(".Task1")
+                .setFlags(FLAG_ACTIVITY_NEW_TASK)
+                .build();
+        doReturn(WINDOWING_MODE_FREEFORM).when(task1).getWindowingMode();
+        mRecentTasks.add(task1);
+        mCallbacksRecorder.clear();
+
+        Task task2 = createTaskBuilder(".Task1")
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK)
+                .build();
+        assertEquals(WINDOWING_MODE_FULLSCREEN, task2.getWindowingMode());
+        mRecentTasks.add(task2);
+
+        assertThat(mCallbacksRecorder.mAdded).hasSize(1);
+        assertThat(mCallbacksRecorder.mAdded).contains(task2);
+        assertThat(mCallbacksRecorder.mTrimmed).isEmpty();
+        assertThat(mCallbacksRecorder.mRemoved).hasSize(1);
+        assertThat(mCallbacksRecorder.mRemoved).contains(task1);
+    }
+
+    @Test
     public void testAddTaskIncompatibleWindowingMode_expectNoRemove() {
         Task task1 = createTaskBuilder(".Task1")
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 7bce828..8fa4667 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4063,8 +4063,9 @@
                         .setInsetsSize(Insets.of(0, 0, 0, 150))
         };
         display.getDisplayPolicy().addWindowLw(navbar, navbar.mAttrs);
-        assertTrue(display.getDisplayPolicy().updateDecorInsetsInfo());
-        display.sendNewConfiguration();
+        if (display.getDisplayPolicy().updateDecorInsetsInfo()) {
+            display.sendNewConfiguration();
+        }
 
         final ActivityRecord activity = getActivityBuilderOnSameTask()
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
@@ -4097,8 +4098,9 @@
                         .setInsetsSize(Insets.of(0, 0, 0, 150))
         };
         display.getDisplayPolicy().addWindowLw(navbar, navbar.mAttrs);
-        assertTrue(display.getDisplayPolicy().updateDecorInsetsInfo());
-        display.sendNewConfiguration();
+        if (display.getDisplayPolicy().updateDecorInsetsInfo()) {
+            display.sendNewConfiguration();
+        }
 
         final ActivityRecord activity = getActivityBuilderOnSameTask()
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
@@ -4126,8 +4128,9 @@
                         .setInsetsSize(Insets.of(0, 0, 0, 150))
         };
         dc.getDisplayPolicy().addWindowLw(navbar, navbar.mAttrs);
-        assertTrue(dc.getDisplayPolicy().updateDecorInsetsInfo());
-        dc.sendNewConfiguration();
+        if (dc.getDisplayPolicy().updateDecorInsetsInfo()) {
+            dc.sendNewConfiguration();
+        }
 
         final ActivityRecord activity = getActivityBuilderOnSameTask()
                 .setResizeMode(RESIZE_MODE_UNRESIZEABLE)
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 4b03483..e4512c3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -18,7 +18,6 @@
 
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -705,50 +704,6 @@
     }
 
     @Test
-    public void testTopActivityEligibleForUserAspectRatioButton() {
-        DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
-        final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
-                .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
-        final Task task = rootTask.getBottomMostTask();
-        final ActivityRecord root = task.getTopNonFinishingActivity();
-        spyOn(mWm.mAppCompatConfiguration);
-        spyOn(root);
-        spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides());
-
-        doReturn(true).when(root).fillsParent();
-        doReturn(true).when(
-                root.mAppCompatController.getAppCompatAspectRatioOverrides())
-                    .shouldEnableUserAspectRatioSettings();
-        doReturn(false).when(root).inSizeCompatMode();
-        doReturn(task).when(root).getOrganizedTask();
-
-        // The button should be eligible to be displayed
-        assertTrue(task.getTaskInfo()
-                .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-
-        // When shouldApplyUserMinAspectRatioOverride is disable the button is not enabled
-        doReturn(false).when(
-                root.mAppCompatController.getAppCompatAspectRatioOverrides())
-                    .shouldEnableUserAspectRatioSettings();
-        assertFalse(task.getTaskInfo()
-                .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-        doReturn(true).when(root.mAppCompatController
-                .getAppCompatAspectRatioOverrides()).shouldEnableUserAspectRatioSettings();
-
-        // When in size compat mode the button is not enabled
-        doReturn(true).when(root).inSizeCompatMode();
-        assertFalse(task.getTaskInfo()
-                .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-        doReturn(false).when(root).inSizeCompatMode();
-
-        // When the top activity is transparent, the button is not enabled
-        doReturn(false).when(root).fillsParent();
-        assertFalse(task.getTaskInfo()
-                .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-        doReturn(true).when(root).fillsParent();
-    }
-
-    @Test
     public void testIsTopActivityTranslucent() {
         DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
         final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
@@ -2112,17 +2067,6 @@
     }
 
     @Test
-    public void getTaskInfoPropagatesCameraCompatMode() {
-        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
-        final ActivityRecord activity = task.getTopMostActivity();
-        activity.mAppCompatController.getAppCompatCameraOverrides().setFreeformCameraCompatMode(
-                CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
-
-        assertEquals(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE,
-                task.getTaskInfo().appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode);
-    }
-
-    @Test
     public void testUpdateTaskDescriptionOnReparent() {
         final Task rootTask1 = createTask(mDisplayContent);
         final Task rootTask2 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index eebb487..9e9874b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -103,8 +103,8 @@
     }
 
     @Override
-    public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
-        return null;
+    public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+        return false;
     }
 
     @Override
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 6c1e1a4..1294945 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -1027,28 +1027,36 @@
             boolean enabled = (mCurrentFunctions & UsbManager.FUNCTION_MIDI) != 0;
             if (enabled != mMidiEnabled) {
                 if (enabled) {
+                    boolean midiDeviceFound = false;
                     if (android.hardware.usb.flags.Flags.enableUsbSysfsMidiIdentification()) {
                         try {
                             getMidiCardDevice();
+                            midiDeviceFound = true;
                         } catch (FileNotFoundException e) {
-                            Slog.e(TAG, "could not identify MIDI device", e);
-                            enabled = false;
+                            Slog.w(TAG, "could not identify MIDI device", e);
                         }
-                    } else {
+                    }
+                    // For backward compatibility with older kernels without
+                    // https://lore.kernel.org/r/20240307030922.3573161-1-royluo@google.com
+                    if (!midiDeviceFound) {
                         Scanner scanner = null;
                         try {
                             scanner = new Scanner(new File(MIDI_ALSA_PATH));
                             mMidiCard = scanner.nextInt();
                             mMidiDevice = scanner.nextInt();
+                            midiDeviceFound = true;
                         } catch (FileNotFoundException e) {
                             Slog.e(TAG, "could not open MIDI file", e);
-                            enabled = false;
                         } finally {
                             if (scanner != null) {
                                 scanner.close();
                             }
                         }
                     }
+                    if (!midiDeviceFound) {
+                        Slog.e(TAG, "Failed to enable MIDI function");
+                        enabled = false;
+                    }
                 }
                 mMidiEnabled = enabled;
             }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index dc5f6e9..fb031bd 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -842,7 +842,7 @@
             return;
         }
 
-        model.setRequested(config.allowMultipleTriggers);
+        model.setRequested(config.isAllowMultipleTriggers());
         // TODO: Remove this block if the lower layer supports multiple triggers.
         if (model.isRequested()) {
             updateRecognitionLocked(model, true);
@@ -964,7 +964,7 @@
         RecognitionConfig config = modelData.getRecognitionConfig();
         if (config != null) {
             // Whether we should continue by starting this again.
-            modelData.setRequested(config.allowMultipleTriggers);
+            modelData.setRequested(config.isAllowMultipleTriggers());
         }
         // TODO: Remove this block if the lower layer supports multiple triggers.
         if (modelData.isRequested()) {
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 862aff9..2bb86bc 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -1439,7 +1439,7 @@
                 runOrAddOperation(new Operation(
                         // always execute:
                         () -> {
-                            if (!mRecognitionConfig.allowMultipleTriggers) {
+                            if (!mRecognitionConfig.isAllowMultipleTriggers()) {
                                 // Unregister this remoteService once op is done
                                 synchronized (mCallbacksLock) {
                                     mCallbacks.remove(mPuuid.getUuid());
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 3e226cc..92effe0 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -19426,4 +19426,48 @@
                 return "UNKNOWN(" + state + ")";
         }
     }
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing. {@code null} to reset.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+    public void setTestEuiccUiComponent(@Nullable ComponentName componentName) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "setTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            telephony.setTestEuiccUiComponent(componentName);
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "setTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing. {@code null} if not available.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @Nullable
+    public ComponentName getTestEuiccUiComponent() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "getTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            return telephony.getTestEuiccUiComponent();
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "getTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index 44d3fca..567314b 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -128,6 +128,12 @@
     /** APN type for RCS (Rich Communication Services). */
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int TYPE_RCS = ApnTypes.RCS;
+    /** APN type for OEM_PAID networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PAID = 1 << 16; // TODO(b/366194627): ApnTypes.OEM_PAID;
+    /** APN type for OEM_PRIVATE networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PRIVATE = 1 << 17; // TODO(b/366194627): ApnTypes.OEM_PRIVATE;
 
     /** @hide */
     @IntDef(flag = true, prefix = {"TYPE_"}, value = {
@@ -146,7 +152,9 @@
             TYPE_BIP,
             TYPE_VSIM,
             TYPE_ENTERPRISE,
-            TYPE_RCS
+            TYPE_RCS,
+            TYPE_OEM_PAID,
+            TYPE_OEM_PRIVATE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ApnType {
@@ -375,6 +383,27 @@
     @SystemApi
     public static final String TYPE_RCS_STRING = "rcs";
 
+    /**
+     * APN type for OEM_PAID networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+
+    /**
+     * APN type for OEM_PRIVATE networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
 
     /** @hide */
     @IntDef(prefix = { "AUTH_TYPE_" }, value = {
@@ -489,6 +518,8 @@
         APN_TYPE_STRING_MAP.put(TYPE_VSIM_STRING, TYPE_VSIM);
         APN_TYPE_STRING_MAP.put(TYPE_BIP_STRING, TYPE_BIP);
         APN_TYPE_STRING_MAP.put(TYPE_RCS_STRING, TYPE_RCS);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PAID_STRING, TYPE_OEM_PAID);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PRIVATE_STRING, TYPE_OEM_PRIVATE);
 
         APN_TYPE_INT_MAP = new ArrayMap<>();
         APN_TYPE_INT_MAP.put(TYPE_DEFAULT, TYPE_DEFAULT_STRING);
@@ -507,6 +538,8 @@
         APN_TYPE_INT_MAP.put(TYPE_VSIM, TYPE_VSIM_STRING);
         APN_TYPE_INT_MAP.put(TYPE_BIP, TYPE_BIP_STRING);
         APN_TYPE_INT_MAP.put(TYPE_RCS, TYPE_RCS_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PAID, TYPE_OEM_PAID_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PRIVATE, TYPE_OEM_PRIVATE_STRING);
 
         PROTOCOL_STRING_MAP = new ArrayMap<>();
         PROTOCOL_STRING_MAP.put("IP", PROTOCOL_IP);
@@ -2383,7 +2416,8 @@
         public ApnSetting build() {
             if ((mApnTypeBitmask & (TYPE_DEFAULT | TYPE_MMS | TYPE_SUPL | TYPE_DUN | TYPE_HIPRI
                     | TYPE_FOTA | TYPE_IMS | TYPE_CBS | TYPE_IA | TYPE_EMERGENCY | TYPE_MCX
-                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS)) == 0
+                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS | TYPE_OEM_PAID
+                    | TYPE_OEM_PRIVATE)) == 0
                 || TextUtils.isEmpty(mApnName) || TextUtils.isEmpty(mEntryName)) {
                 return null;
             }
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 4eefaac..bd5c759 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1113,6 +1113,12 @@
      * @hide
      */
     public static final int DATAGRAM_TYPE_SMS = 6;
+    /**
+     * Datagram type indicating that the message to be sent is an SMS checking
+     * for pending incoming SMS.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7;
 
     /** @hide */
     @IntDef(prefix = "DATAGRAM_TYPE_", value = {
@@ -1122,7 +1128,8 @@
             DATAGRAM_TYPE_KEEP_ALIVE,
             DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP,
             DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED,
-            DATAGRAM_TYPE_SMS
+            DATAGRAM_TYPE_SMS,
+            DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatagramType {}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index e57c207..7f25ef2 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3017,6 +3017,14 @@
     boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis);
 
     /**
+     * This API can be used by only CTS to control ingoring cellular service state event.
+     *
+     * @param enabled Whether to enable boolean config.
+     * @return {@code true} if the value is set successfully, {@code false} otherwise.
+     */
+    boolean setSatelliteIgnoreCellularServiceState(in boolean enabled);
+
+    /**
      * This API can be used by only CTS to update satellite pointing UI app package and class names.
      *
      * @param packageName The package name of the satellite pointing UI app.
@@ -3409,4 +3417,20 @@
      * @hide
      */
     boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name);
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing
+     * @hide
+     */
+    void setTestEuiccUiComponent(in ComponentName componentName);
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing.
+     * @hide
+     */
+    ComponentName getTestEuiccUiComponent();
 }
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index ba36007..4ae06a4 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import android.content.ContextWrapper
+import android.content.pm.PackageManager
+import android.content.res.Resources
 import android.hardware.input.IInputManager
 import android.hardware.input.AidlKeyGestureEvent
 import android.hardware.input.IKeyGestureEventListener
@@ -25,15 +27,20 @@
 import android.hardware.input.InputManager
 import android.hardware.input.InputManagerGlobal
 import android.hardware.input.KeyGestureEvent
-import android.hardware.input.KeyGestureEvent.KeyGestureType
 import android.os.IBinder
 import android.os.Process
+import android.os.SystemClock
+import android.os.SystemProperties
 import android.os.test.TestLooper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
 import android.view.InputDevice
-import android.view.KeyCharacterMap
 import android.view.KeyEvent
+import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE
 import androidx.test.core.app.ApplicationProvider
+import com.android.internal.R
 import com.android.internal.annotations.Keep
 import com.android.internal.util.FrameworkStatsLog
 import com.android.modules.utils.testing.ExtendedMockitoRule
@@ -78,32 +85,60 @@
             KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON),
             KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON),
         )
+        const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0
+        const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1
+        const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
+        const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
+        const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
     }
 
     @JvmField
     @Rule
     val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
-        .mockStatic(FrameworkStatsLog::class.java).build()!!
+        .mockStatic(FrameworkStatsLog::class.java)
+        .mockStatic(SystemProperties::class.java).build()!!
+
+    @JvmField
+    @Rule
+    val rule = SetFlagsRule()
 
     @Mock
     private lateinit var iInputManager: IInputManager
 
+    @Mock
+    private lateinit var resources: Resources
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+
     private var currentPid = 0
-    private lateinit var keyGestureController: KeyGestureController
     private lateinit var context: Context
     private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
     private lateinit var testLooper: TestLooper
     private var events = mutableListOf<KeyGestureEvent>()
-    private var handleEvents = mutableListOf<KeyGestureEvent>()
 
     @Before
     fun setup() {
         context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+        Mockito.`when`(context.resources).thenReturn(resources)
         inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
         setupInputDevices()
+        setupBehaviors()
         testLooper = TestLooper()
         currentPid = Process.myPid()
-        keyGestureController = KeyGestureController(context, testLooper.looper)
+    }
+
+    private fun setupBehaviors() {
+        Mockito.`when`(
+            resources.getBoolean(
+                com.android.internal.R.bool.config_enableScreenshotChord
+            )
+        ).thenReturn(true)
+        Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH))
+            .thenReturn(true)
+        Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
+            .thenReturn(true)
+        Mockito.`when`(context.packageManager).thenReturn(packageManager)
     }
 
     private fun setupInputDevices() {
@@ -116,19 +151,22 @@
         Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
     }
 
-    private fun notifyHomeGestureCompleted() {
-        keyGestureController.notifyKeyGestureCompleted(DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
+    private fun notifyHomeGestureCompleted(keyGestureController: KeyGestureController) {
+        keyGestureController.notifyKeyGestureCompleted(
+            DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
             KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
-            KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
+            KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+        )
     }
 
     @Test
     fun testKeyGestureEvent_registerUnregisterListener() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
         val listener = KeyGestureEventListener()
 
         // Register key gesture event listener
         keyGestureController.registerKeyGestureEventListener(listener, 0)
-        notifyHomeGestureCompleted()
+        notifyHomeGestureCompleted(keyGestureController)
         testLooper.dispatchAll()
         assertEquals(
             "Listener should get callbacks on key gesture event completed",
@@ -144,7 +182,7 @@
         // Unregister listener
         events.clear()
         keyGestureController.unregisterKeyGestureEventListener(listener, 0)
-        notifyHomeGestureCompleted()
+        notifyHomeGestureCompleted(keyGestureController)
         testLooper.dispatchAll()
         assertEquals(
             "Listener should not get callback after being unregistered",
@@ -155,20 +193,22 @@
 
     @Test
     fun testKeyGestureEvent_multipleGestureHandlers() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+
         // Set up two callbacks.
         var callbackCount1 = 0
         var callbackCount2 = 0
         var selfCallback = 0
         val externalHandler1 = KeyGestureHandler { _, _ ->
-            callbackCount1++;
+            callbackCount1++
             true
         }
         val externalHandler2 = KeyGestureHandler { _, _ ->
-            callbackCount2++;
+            callbackCount2++
             true
         }
         val selfHandler = KeyGestureHandler { _, _ ->
-            selfCallback++;
+            selfCallback++
             false
         }
 
@@ -406,6 +446,14 @@
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
             ),
             TestData(
+                "META + / -> Open Shortcut Helper",
+                intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH),
+                KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+                intArrayOf(KeyEvent.KEYCODE_SLASH),
+                KeyEvent.META_META_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
                 "BRIGHTNESS_UP -> Brightness Up",
                 intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP),
                 KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
@@ -528,12 +576,314 @@
                     KeyGestureEvent.ACTION_GESTURE_COMPLETE
                 )
             ),
+            TestData(
+                "CTRL + SPACE -> Switch Language Forward",
+                intArrayOf(KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SPACE),
+                KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                intArrayOf(KeyEvent.KEYCODE_SPACE),
+                KeyEvent.META_CTRL_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "CTRL + SHIFT + SPACE -> Switch Language Backward",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_SHIFT_LEFT,
+                    KeyEvent.KEYCODE_SPACE
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                intArrayOf(KeyEvent.KEYCODE_SPACE),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "CTRL + ALT + Z -> Accessibility Shortcut",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_Z
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+                intArrayOf(KeyEvent.KEYCODE_Z),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "SYSRQ -> Take screenshot",
+                intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+                intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "ESC -> Close All Dialogs",
+                intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+                KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+                intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }
 
     @Test
     @Parameters(method = "keyGestureEventHandlerTestArguments")
     fun testKeyGestures(test: TestData) {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(keyGestureController, test)
+    }
+
+    @Test
+    fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        val testKeys = intArrayOf(
+            KeyEvent.KEYCODE_RECENT_APPS,
+            KeyEvent.KEYCODE_APP_SWITCH,
+            KeyEvent.KEYCODE_BRIGHTNESS_UP,
+            KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
+            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
+            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
+            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
+            KeyEvent.KEYCODE_ALL_APPS,
+            KeyEvent.KEYCODE_NOTIFICATION,
+            KeyEvent.KEYCODE_SETTINGS,
+            KeyEvent.KEYCODE_LANGUAGE_SWITCH,
+            KeyEvent.KEYCODE_SCREENSHOT,
+            KeyEvent.KEYCODE_META_LEFT,
+            KeyEvent.KEYCODE_META_RIGHT,
+            KeyEvent.KEYCODE_ASSIST,
+            KeyEvent.KEYCODE_VOICE_ASSIST,
+            KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
+            KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
+            KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
+            KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
+        )
+
+        val handler = KeyGestureHandler { _, _ -> false }
+        keyGestureController.registerKeyGestureHandler(handler, 0)
+
+        for (key in testKeys) {
+            sendKeys(keyGestureController, intArrayOf(key), assertNotSentToApps = true)
+        }
+    }
+
+    @Test
+    fun testSearchKeyGestures_defaultSearch() {
+        Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+            .thenReturn(SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH)
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureNotProduced(
+            keyGestureController,
+            "SEARCH -> Default Search",
+            intArrayOf(KeyEvent.KEYCODE_SEARCH),
+        )
+    }
+
+    @Test
+    fun testSearchKeyGestures_searchActivity() {
+        Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+            .thenReturn(SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY)
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "SEARCH -> Launch Search Activity",
+                intArrayOf(KeyEvent.KEYCODE_SEARCH),
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+                intArrayOf(KeyEvent.KEYCODE_SEARCH),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    fun testSettingKeyGestures_doNothing() {
+        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+            .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTHING)
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureNotProduced(
+            keyGestureController,
+            "SETTINGS -> Do Nothing",
+            intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+        )
+    }
+
+    @Test
+    fun testSettingKeyGestures_settingsActivity() {
+        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+            .thenReturn(SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY)
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "SETTINGS -> Launch Settings Activity",
+                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    fun testSettingKeyGestures_notificationPanel() {
+        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+            .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL)
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "SETTINGS -> Toggle Notification Panel",
+                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+    fun testTriggerBugReport() {
+        Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "META + CTRL + DEL -> Trigger Bug Report",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_DEL
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
+                intArrayOf(KeyEvent.KEYCODE_DEL),
+                KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+    fun testTriggerBugReport_flagDisabled() {
+        Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "META + CTRL + DEL -> Not Trigger Bug Report (Fallback to BACK)",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_DEL
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+                intArrayOf(KeyEvent.KEYCODE_DEL),
+                KeyEvent.META_META_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    fun testCapsLockPressNotified() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        val listener = KeyGestureEventListener()
+
+        keyGestureController.registerKeyGestureEventListener(listener, 0)
+        sendKeys(keyGestureController, intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK))
+        testLooper.dispatchAll()
+        assertEquals(
+            "Listener should get callbacks on key gesture event completed",
+            1,
+            events.size
+        )
+        assertEquals(
+            "Listener should get callback for Toggle Caps Lock key gesture complete event",
+            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+            events[0].keyGestureType
+        )
+    }
+
+    @Keep
+    private fun keyGestureEventHandlerTestArguments_forKeyCombinations(): Array<TestData> {
+        return arrayOf(
+            TestData(
+                "VOLUME_DOWN + POWER -> Screenshot Chord",
+                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+                KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+                0,
+                intArrayOf(
+                    KeyGestureEvent.ACTION_GESTURE_START,
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                )
+            ),
+            TestData(
+                "POWER + STEM_PRIMARY -> Screenshot Chord",
+                intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+                KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+                0,
+                intArrayOf(
+                    KeyGestureEvent.ACTION_GESTURE_START,
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                )
+            ),
+            TestData(
+                "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord",
+                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+                0,
+                intArrayOf(
+                    KeyGestureEvent.ACTION_GESTURE_START,
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                )
+            ),
+            TestData(
+                "BACK + DPAD_DOWN -> TV Accessibility Chord",
+                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+                0,
+                intArrayOf(
+                    KeyGestureEvent.ACTION_GESTURE_START,
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                )
+            ),
+            TestData(
+                "BACK + DPAD_CENTER -> TV Trigger Bug Report",
+                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+                0,
+                intArrayOf(
+                    KeyGestureEvent.ACTION_GESTURE_START,
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                )
+            ),
+        )
+    }
+
+    @Test
+    @Parameters(method = "keyGestureEventHandlerTestArguments_forKeyCombinations")
+    @EnableFlags(
+        com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
+        com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES
+    )
+    fun testKeyCombinationGestures(test: TestData) {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(keyGestureController, test)
+    }
+
+    private fun testKeyGestureInternal(keyGestureController: KeyGestureController, test: TestData) {
+        var handleEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
             handleEvents.add(KeyGestureEvent(event))
             true
@@ -541,7 +891,7 @@
         keyGestureController.registerKeyGestureHandler(handler, 0)
         handleEvents.clear()
 
-        sendKeys(test.keys, /* assertAllConsumed = */ false)
+        sendKeys(keyGestureController, test.keys)
 
         assertEquals(
             "Test: $test doesn't produce correct number of key gesture events",
@@ -575,55 +925,37 @@
         keyGestureController.unregisterKeyGestureHandler(handler, 0)
     }
 
-    @Test
-    fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
-        val testKeys = intArrayOf(
-            KeyEvent.KEYCODE_RECENT_APPS,
-            KeyEvent.KEYCODE_APP_SWITCH,
-            KeyEvent.KEYCODE_BRIGHTNESS_UP,
-            KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
-            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
-            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
-            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
-            KeyEvent.KEYCODE_ALL_APPS,
-            KeyEvent.KEYCODE_NOTIFICATION,
-            KeyEvent.KEYCODE_SETTINGS,
-            KeyEvent.KEYCODE_LANGUAGE_SWITCH,
-            KeyEvent.KEYCODE_SCREENSHOT,
-            KeyEvent.KEYCODE_META_LEFT,
-            KeyEvent.KEYCODE_META_RIGHT,
-            KeyEvent.KEYCODE_ASSIST,
-            KeyEvent.KEYCODE_VOICE_ASSIST,
-            KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
-            KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
-            KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
-            KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
-        )
-
-        val handler = KeyGestureHandler { _, _ -> false }
-        keyGestureController.registerKeyGestureHandler(handler, 0)
-
-        for (key in testKeys) {
-            sendKeys(intArrayOf(key), /* assertAllConsumed = */ true)
+    private fun testKeyGestureNotProduced(
+        keyGestureController: KeyGestureController,
+        testName: String,
+        testKeys: IntArray
+    ) {
+        var handleEvents = mutableListOf<KeyGestureEvent>()
+        val handler = KeyGestureHandler { event, _ ->
+            handleEvents.add(KeyGestureEvent(event))
+            true
         }
+        keyGestureController.registerKeyGestureHandler(handler, 0)
+        handleEvents.clear()
+
+        sendKeys(keyGestureController, testKeys)
+        assertEquals("Test: $testName should not produce Key gesture", 0, handleEvents.size)
     }
 
-    private fun sendKeys(testKeys: IntArray, assertAllConsumed: Boolean) {
+    private fun sendKeys(
+        keyGestureController: KeyGestureController,
+        testKeys: IntArray,
+        assertNotSentToApps: Boolean = false
+    ) {
         var metaState = 0
+        val now = SystemClock.uptimeMillis()
         for (key in testKeys) {
             val downEvent = KeyEvent(
-                /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_DOWN, key,
-                0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
-                0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+                now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState,
+                DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+                InputDevice.SOURCE_KEYBOARD
             )
-            val consumed =
-                keyGestureController.interceptKeyBeforeDispatching(null, downEvent, 0) == -1L
-            if (assertAllConsumed) {
-                assertTrue(
-                    "interceptKeyBeforeDispatching should consume all events $downEvent",
-                    consumed
-                )
-            }
+            interceptKey(keyGestureController, downEvent, assertNotSentToApps)
             metaState = metaState or MODIFIER.getOrDefault(key, 0)
 
             downEvent.recycle()
@@ -632,24 +964,39 @@
 
         for (key in testKeys.reversed()) {
             val upEvent = KeyEvent(
-                /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_UP, key,
-                0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
-                0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+                now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
+                DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+                InputDevice.SOURCE_KEYBOARD
             )
-            val consumed =
-                keyGestureController.interceptKeyBeforeDispatching(null, upEvent, 0) == -1L
-            if (assertAllConsumed) {
-                assertTrue(
-                    "interceptKeyBeforeDispatching should consume all events $upEvent",
-                    consumed
-                )
-            }
+            interceptKey(keyGestureController, upEvent, assertNotSentToApps)
+            metaState = metaState and MODIFIER.getOrDefault(key, 0).inv()
 
             upEvent.recycle()
             testLooper.dispatchAll()
         }
     }
 
+    private fun interceptKey(
+        keyGestureController: KeyGestureController,
+        event: KeyEvent,
+        assertNotSentToApps: Boolean
+    ) {
+        keyGestureController.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE)
+        testLooper.dispatchAll()
+
+        val consumed =
+            keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L
+        if (assertNotSentToApps) {
+            assertTrue(
+                "interceptKeyBeforeDispatching should consume all events $event",
+                consumed
+            )
+        }
+        if (!consumed) {
+            keyGestureController.interceptUnhandledKey(event, null)
+        }
+    }
+
     inner class KeyGestureEventListener : IKeyGestureEventListener.Stub() {
         override fun onKeyGestureEvent(event: AidlKeyGestureEvent) {
             events.add(KeyGestureEvent(event))
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index b5258df..60fa52f 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -402,4 +402,73 @@
         // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
         verify(mWindowManager, times(0)).updateViewLayout(any(), any());
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testPinchDrag() {
+        float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10;
+
+        MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionDown);
+
+        MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN,
+                SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f)
+                )
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(pointerDown);
+
+        // Simulate ACTION_MOVE event (both fingers moving apart).
+        MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .rawXCursorPosition(mWindowLayoutParams.x + 10f)
+                .rawYCursorPosition(mWindowLayoutParams.y + 10f)
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f + offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionMove);
+
+        MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f + offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(pointerUp);
+
+        MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionUp);
+
+        // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
+        verify(mWindowManager, times(0)).updateViewLayout(any(), any());
+    }
+}
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 3e58517..9f35c7b 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -32,6 +32,27 @@
     test_suites: ["device-tests"],
 }
 
+// Run just ApplicationSharedMemoryTest with ABI override for 32 bits.
+// This is to test that on systems that support multi-ABI,
+// ApplicationSharedMemory works in app processes launched with a different ABI
+// than that of the system processes.
+android_test {
+    name: "ApplicationSharedMemoryTest32",
+    team: "trendy_team_system_performance",
+    srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTest.java"],
+    libs: ["android.test.runner.stubs.system"],
+    static_libs: [
+        "junit",
+        "androidx.test.rules",
+        "platform-test-annotations",
+    ],
+    manifest: "ApplicationSharedMemoryTest32/AndroidManifest.xml",
+    test_config: "ApplicationSharedMemoryTest32/AndroidTest.xml",
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+}
+
 android_ravenwood_test {
     name: "InternalTestsRavenwood",
     static_libs: [
@@ -45,3 +66,9 @@
     ],
     auto_gen_config: true,
 }
+
+java_test_helper_library {
+    name: "ApplicationSharedMemoryTestRule",
+    srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTestRule.java"],
+    static_libs: ["junit"],
+}
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml
new file mode 100644
index 0000000..4e1058e
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.internal.tests">
+    <application
+        android:use32bitAbi="true"
+        android:multiArch="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.internal.tests"
+         android:label="Internal Tests"/>
+</manifest>
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml
new file mode 100644
index 0000000..9bde8b7
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+<configuration description="Runs tests for internal classes/utilities.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="ApplicationSharedMemoryTest32.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="framework-base-presubmit" />
+    <option name="test-tag" value="InternalTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.internal.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path"/>
+        <option name="directory-keys"
+            value="/data/user/0/com.android.internal.tests/files"/>
+        <option name="collect-on-run-ended-only" value="true"/>
+        <option name="clean-up" value="true"/>
+    </metrics_collector>
+</configuration>
\ No newline at end of file
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/OWNERS b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS
new file mode 100644
index 0000000..1ff3fac
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/PERFORMANCE_OWNERS
\ No newline at end of file
diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
new file mode 100644
index 0000000..e3a129f
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.internal.os;
+
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.Before;
+
+import java.io.FileDescriptor;
+
+/** Tests for {@link TimeoutRecord}. */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class ApplicationSharedMemoryTest {
+
+    @Before
+    public void setUp() {
+        // Skip tests if the feature under test is disabled.
+        assumeTrue(Flags.applicationSharedMemoryEnabled());
+    }
+
+    /**
+     * Every application process, including ours, should have had an instance installed at this
+     * point.
+     */
+    @Test
+    public void hasInstance() {
+        // This shouldn't throw and shouldn't return null.
+        assertNotNull(ApplicationSharedMemory.getInstance());
+    }
+
+    /** Any app process should be able to read shared memory values. */
+    @Test
+    public void canRead() {
+        ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance();
+        instance.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+        // Don't actually care about the value of the above.
+    }
+
+    /** Application processes should not have mutable access. */
+    @Test
+    public void appInstanceNotMutable() {
+        ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance();
+        try {
+            instance.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17);
+            fail("Attempted mutation in an app process should throw");
+        } catch (Exception expected) {
+        }
+    }
+
+    /** Instances share memory if they share the underlying memory region. */
+    @Test
+    public void instancesShareMemory() throws IOException {
+        ApplicationSharedMemory instance1 = ApplicationSharedMemory.create();
+        ApplicationSharedMemory instance2 =
+                ApplicationSharedMemory.fromFileDescriptor(
+                        instance1.getFileDescriptor(), /* mutable= */ true);
+        ApplicationSharedMemory instance3 =
+                ApplicationSharedMemory.fromFileDescriptor(
+                        instance2.getReadOnlyFileDescriptor(), /* mutable= */ false);
+
+        instance1.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17);
+        assertEquals(
+                17, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                17, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                17, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+
+        instance2.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(24);
+        assertEquals(
+                24, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                24, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                24, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+    }
+
+    /** Can't map read-only memory as mutable. */
+    @Test
+    public void readOnlyCantBeMutable() throws IOException {
+        ApplicationSharedMemory readWriteInstance = ApplicationSharedMemory.create();
+        FileDescriptor readOnlyFileDescriptor = readWriteInstance.getReadOnlyFileDescriptor();
+
+        try {
+            ApplicationSharedMemory.fromFileDescriptor(readOnlyFileDescriptor, /* mutable= */ true);
+            fail("Shouldn't be able to map read-only memory as mutable");
+        } catch (Exception expected) {
+        }
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java
new file mode 100644
index 0000000..ff2a461
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java
@@ -0,0 +1,56 @@
+/*
+ * 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.internal.os;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import com.android.internal.os.ApplicationSharedMemory;
+
+/** Test rule that sets up and tears down ApplicationSharedMemory for test. */
+public class ApplicationSharedMemoryTestRule implements TestRule {
+
+    private ApplicationSharedMemory mSavedInstance;
+
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                setup();
+                try {
+                    base.evaluate(); // Run the test
+                } finally {
+                    teardown();
+                }
+            }
+        };
+    }
+
+    private void setup() {
+        mSavedInstance = ApplicationSharedMemory.sInstance;
+        ApplicationSharedMemory.sInstance = ApplicationSharedMemory.create();
+    }
+
+    private void teardown() {
+        ApplicationSharedMemory.sInstance.close();
+        ApplicationSharedMemory.sInstance = mSavedInstance;
+        mSavedInstance = null;
+    }
+}
diff --git a/tests/Tracing/TEST_MAPPING b/tests/Tracing/TEST_MAPPING
index 7f58fce..f6e5221 100644
--- a/tests/Tracing/TEST_MAPPING
+++ b/tests/Tracing/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-  "postsubmit": [
+  "presubmit": [
     {
       "name": "TracingTests"
     }
diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp
index 7596ee7..f211185 100644
--- a/tests/testables/Android.bp
+++ b/tests/testables/Android.bp
@@ -25,7 +25,10 @@
 
 java_library {
     name: "testables",
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     libs: [
         "android.test.runner.stubs.system",
         "android.test.mock.stubs.system",
diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java
index 37b39c3..10df17f 100644
--- a/tests/testables/src/android/testing/TestWithLooperRule.java
+++ b/tests/testables/src/android/testing/TestWithLooperRule.java
@@ -34,13 +34,13 @@
  * Looper for the Statement.
  */
 public class TestWithLooperRule implements MethodRule {
-
     /*
      * This rule requires to be the inner most Rule, so the next statement is RunAfters
      * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)'
      */
     @Override
     public Statement apply(Statement base, FrameworkMethod method, Object target) {
+
         // getting testRunner check, if AndroidTestingRunning then we skip this rule
         RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class);
         if (runWithAnnotation != null) {
@@ -97,6 +97,9 @@
                     case "InvokeParameterizedMethod":
                         this.wrapFieldMethodFor(next, "frameworkMethod", method, target);
                         return;
+                    case "ExpectException":
+                        next = this.getNextStatement(next, "next");
+                        break;
                     default:
                         throw new Exception(
                                 String.format("Unexpected Statement received: [%s]",
diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp
index 1eb36fa..c23f41a 100644
--- a/tests/testables/tests/Android.bp
+++ b/tests/testables/tests/Android.bp
@@ -34,6 +34,7 @@
         "androidx.core_core-animation",
         "androidx.core_core-ktx",
         "androidx.test.rules",
+        "androidx.test.ext.junit",
         "hamcrest-library",
         "mockito-target-inline-minus-junit4",
         "testables",
diff --git a/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java
new file mode 100644
index 0000000..b7d5e0e
--- /dev/null
+++ b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.testing;
+
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that TestableLooper now handles expected exceptions in tests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RunWithLooper
+public class TestableLooperJUnit4Test {
+    @Rule
+    public final TestWithLooperRule mTestWithLooperRule = new TestWithLooperRule();
+
+    @Test(expected = Exception.class)
+    public void testException() throws Exception {
+        throw new Exception("this exception is expected");
+    }
+}
+
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index df1d51e..064b461 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -346,6 +346,21 @@
             value->value->Accept(&body_printer);
             printer->Undent();
           }
+          printer->Println("Flag disabled values:");
+          for (const auto& value : entry.flag_disabled_values) {
+            printer->Print("(");
+            printer->Print(value->config.to_string());
+            printer->Print(") ");
+            value->value->Accept(&headline_printer);
+            if (options.show_sources && !value->value->GetSource().path.empty()) {
+              printer->Print(" src=");
+              printer->Print(value->value->GetSource().to_string());
+            }
+            printer->Println();
+            printer->Indent();
+            value->value->Accept(&body_printer);
+            printer->Undent();
+          }
           printer->Undent();
         }
       }
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index a274f04..0d261ab 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -71,6 +71,17 @@
 
 enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 };
 
+struct FeatureFlagAttribute {
+  std::string name;
+  bool negated = false;
+
+  std::string ToString() {
+    return (negated ? "!" : "") + name;
+  }
+
+  bool operator==(const FeatureFlagAttribute& o) const = default;
+};
+
 android::StringPiece to_string(ResourceType type);
 
 /**
@@ -232,6 +243,12 @@
 
   // Exported symbols
   std::vector<SourcedResourceName> exported_symbols;
+
+  // Flag status
+  FlagStatus flag_status = FlagStatus::NoFlag;
+
+  // Flag
+  std::optional<FeatureFlagAttribute> flag;
 };
 
 /**
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index a5aecc8..fce6aa7 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -107,9 +107,10 @@
   Visibility::Level visibility_level = Visibility::Level::kUndefined;
   bool staged_api = false;
   bool allow_new = false;
-  FlagStatus flag_status = FlagStatus::NoFlag;
   std::optional<OverlayableItem> overlayable_item;
   std::optional<StagedId> staged_alias;
+  std::optional<FeatureFlagAttribute> flag;
+  FlagStatus flag_status;
 
   std::string comment;
   std::unique_ptr<Value> value;
@@ -151,6 +152,7 @@
   }
 
   if (res->value != nullptr) {
+    res->value->SetFlag(res->flag);
     res->value->SetFlagStatus(res->flag_status);
     // Attach the comment, source and config to the value.
     res->value->SetComment(std::move(res->comment));
@@ -162,8 +164,6 @@
     res_builder.SetStagedId(res->staged_alias.value());
   }
 
-  res_builder.SetFlagStatus(res->flag_status);
-
   bool error = false;
   if (!res->name.entry.empty()) {
     if (!table->AddResource(res_builder.Build(), diag)) {
@@ -546,12 +546,26 @@
       {"symbol", std::mem_fn(&ResourceParser::ParseSymbol)},
   });
 
-  std::string resource_type = parser->element_name();
-  auto flag_status = GetFlagStatus(parser);
-  if (!flag_status) {
-    return false;
+  std::string_view resource_type = parser->element_name();
+  if (auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"))) {
+    if (options_.flag) {
+      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
+                   << "Resource flag are not allowed both in the path and in the file");
+      return false;
+    }
+    out_resource->flag = std::move(flag);
+    std::string error;
+    auto flag_status = GetFlagStatus(out_resource->flag, options_.feature_flag_values, &error);
+    if (flag_status) {
+      out_resource->flag_status = flag_status.value();
+    } else {
+      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error);
+      return false;
+    }
+  } else if (options_.flag) {
+    out_resource->flag = options_.flag;
+    out_resource->flag_status = options_.flag_status;
   }
-  out_resource->flag_status = flag_status.value();
 
   // The value format accepted for this resource.
   uint32_t resource_format = 0u;
@@ -567,7 +581,7 @@
 
     // Items have their type encoded in the type attribute.
     if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) {
-      resource_type = std::string(maybe_type.value());
+      resource_type = maybe_type.value();
     } else {
       diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
                    << "<item> must have a 'type' attribute");
@@ -590,7 +604,7 @@
 
     // Bags have their type encoded in the type attribute.
     if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) {
-      resource_type = std::string(maybe_type.value());
+      resource_type = maybe_type.value();
     } else {
       diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
                    << "<bag> must have a 'type' attribute");
@@ -733,33 +747,6 @@
   return false;
 }
 
-std::optional<FlagStatus> ResourceParser::GetFlagStatus(xml::XmlPullParser* parser) {
-  auto flag_status = FlagStatus::NoFlag;
-
-  std::optional<StringPiece> flag = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag");
-  if (flag) {
-    auto flag_it = options_.feature_flag_values.find(flag.value());
-    if (flag_it == options_.feature_flag_values.end()) {
-      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
-                   << "Resource flag value undefined");
-      return {};
-    }
-    const auto& flag_properties = flag_it->second;
-    if (!flag_properties.read_only) {
-      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
-                   << "Only read only flags may be used with resources");
-      return {};
-    }
-    if (!flag_properties.enabled.has_value()) {
-      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
-                   << "Only flags with a value may be used with resources");
-      return {};
-    }
-    flag_status = flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled;
-  }
-  return flag_status;
-}
-
 bool ResourceParser::ParseItem(xml::XmlPullParser* parser,
                                ParsedResource* out_resource,
                                const uint32_t format) {
@@ -1666,21 +1653,25 @@
     const std::string& element_namespace = parser->element_namespace();
     const std::string& element_name = parser->element_name();
     if (element_namespace.empty() && element_name == "item") {
-      auto flag_status = GetFlagStatus(parser);
-      if (!flag_status) {
-        error = true;
-        continue;
-      }
+      auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"));
       std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString);
       if (!item) {
         diag_->Error(android::DiagMessage(item_source) << "could not parse array item");
         error = true;
         continue;
       }
-      item->SetFlagStatus(flag_status.value());
+      item->SetFlag(flag);
+      std::string err;
+      auto status = GetFlagStatus(flag, options_.feature_flag_values, &err);
+      if (status) {
+        item->SetFlagStatus(status.value());
+      } else {
+        diag_->Error(android::DiagMessage(item_source) << err);
+        error = true;
+        continue;
+      }
       item->SetSource(item_source);
       array->elements.emplace_back(std::move(item));
-
     } else if (!ShouldIgnoreElement(element_namespace, element_name)) {
       diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
                    << "unknown tag <" << element_namespace << ":" << element_name << ">");
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 442dea8..90690d5 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -57,6 +57,11 @@
   std::optional<Visibility::Level> visibility;
 
   FeatureFlagValues feature_flag_values;
+
+  // The flag that should be applied to all resources parsed
+  std::optional<FeatureFlagAttribute> flag;
+
+  FlagStatus flag_status = FlagStatus::NoFlag;
 };
 
 struct FlattenedXmlSubTree {
@@ -85,8 +90,6 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(ResourceParser);
 
-  std::optional<FlagStatus> GetFlagStatus(xml::XmlPullParser* parser);
-
   std::optional<FlattenedXmlSubTree> CreateFlattenSubTree(xml::XmlPullParser* parser);
 
   // Parses the XML subtree as a StyleString (flattened XML representation for strings with
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 9751459..5435cba2 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -101,6 +101,21 @@
   }
 };
 
+struct ConfigFlagKey {
+  const ConfigDescription* config;
+  StringPiece product;
+  const FeatureFlagAttribute& flag;
+};
+
+struct lt_config_flag_key_ref {
+  template <typename T>
+  bool operator()(const T& lhs, const ConfigFlagKey& rhs) const noexcept {
+    return std::tie(lhs->config, lhs->product, lhs->value->GetFlag()->name,
+                    lhs->value->GetFlag()->negated) <
+           std::tie(*rhs.config, rhs.product, rhs.flag.name, rhs.flag.negated);
+  }
+};
+
 }  // namespace
 
 ResourceTable::ResourceTable(ResourceTable::Validation validation) : validation_(validation) {
@@ -213,6 +228,25 @@
   return results;
 }
 
+ResourceConfigValue* ResourceEntry::FindOrCreateFlagDisabledValue(
+    const FeatureFlagAttribute& flag, const android::ConfigDescription& config,
+    android::StringPiece product) {
+  auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+                               ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+  if (iter != flag_disabled_values.end()) {
+    ResourceConfigValue* value = iter->get();
+    const auto value_flag = value->value->GetFlag().value();
+    if (value_flag.name == flag.name && value_flag.negated == flag.negated &&
+        value->config == config && value->product == product) {
+      return value;
+    }
+  }
+  ResourceConfigValue* newValue =
+      flag_disabled_values.insert(iter, util::make_unique<ResourceConfigValue>(config, product))
+          ->get();
+  return newValue;
+}
+
 bool ResourceEntry::HasDefaultValue() const {
   // The default config should be at the top of the list, since the list is sorted.
   return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig();
@@ -375,13 +409,14 @@
   }
 };
 
-void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePackage* package,
-                              const ResourceTableType* type, const std::string& entry_name,
-                              const std::optional<ResourceId>& id, const Visibility& visibility,
-                              const std::optional<AllowNew>& allow_new,
-                              const std::optional<OverlayableItem>& overlayable_item,
-                              const std::optional<StagedId>& staged_id,
-                              const std::vector<std::unique_ptr<ResourceConfigValue>>& values) {
+void InsertEntryIntoTableView(
+    ResourceTableView& table, const ResourceTablePackage* package, const ResourceTableType* type,
+    const std::string& entry_name, const std::optional<ResourceId>& id,
+    const Visibility& visibility, const std::optional<AllowNew>& allow_new,
+    const std::optional<OverlayableItem>& overlayable_item,
+    const std::optional<StagedId>& staged_id,
+    const std::vector<std::unique_ptr<ResourceConfigValue>>& values,
+    const std::vector<std::unique_ptr<ResourceConfigValue>>& flag_disabled_values) {
   SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter;
   SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter;
   SortedVectorInserter<ResourceTableEntryView, EntryViewComparer> entry_inserter;
@@ -408,6 +443,9 @@
   for (auto& value : values) {
     new_entry.values.emplace_back(value.get());
   }
+  for (auto& value : flag_disabled_values) {
+    new_entry.flag_disabled_values.emplace_back(value.get());
+  }
 
   entry_inserter.Insert(view_type->entries, std::move(new_entry));
 }
@@ -426,6 +464,21 @@
   return nullptr;
 }
 
+const ResourceConfigValue* ResourceTableEntryView::FindFlagDisabledValue(
+    const FeatureFlagAttribute& flag, const ConfigDescription& config,
+    android::StringPiece product) const {
+  auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+                               ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+  if (iter != values.end()) {
+    const ResourceConfigValue* value = *iter;
+    if (value->value->GetFlag() == flag && value->config == config &&
+        StringPiece(value->product) == product) {
+      return value;
+    }
+  }
+  return nullptr;
+}
+
 ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptions& options) const {
   ResourceTableView view;
   for (const auto& package : packages) {
@@ -433,13 +486,13 @@
       for (const auto& entry : type->entries) {
         InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, entry->id,
                                  entry->visibility, entry->allow_new, entry->overlayable_item,
-                                 entry->staged_id, entry->values);
+                                 entry->staged_id, entry->values, entry->flag_disabled_values);
 
         if (options.create_alias_entries && entry->staged_id) {
           auto alias_id = entry->staged_id.value().id;
           InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, alias_id,
                                    entry->visibility, entry->allow_new, entry->overlayable_item, {},
-                                   entry->values);
+                                   entry->values, entry->flag_disabled_values);
         }
       }
     }
@@ -587,6 +640,25 @@
     entry->staged_id = res.staged_id.value();
   }
 
+  if (res.value != nullptr && res.value->GetFlagStatus() == FlagStatus::Disabled) {
+    auto disabled_config_value =
+        entry->FindOrCreateFlagDisabledValue(res.value->GetFlag().value(), res.config, res.product);
+    if (!disabled_config_value->value) {
+      // Resource does not exist, add it now.
+      // Must clone the value since it might be in the values vector as well
+      CloningValueTransformer cloner(&string_pool);
+      disabled_config_value->value = res.value->Transform(cloner);
+    } else {
+      diag->Error(android::DiagMessage(source)
+                  << "duplicate value for resource '" << res.name << "' " << "with config '"
+                  << res.config << "' and flag '"
+                  << (res.value->GetFlag().value().negated ? "!" : "")
+                  << res.value->GetFlag().value().name << "'");
+      diag->Error(android::DiagMessage(source) << "resource previously defined here");
+      return false;
+    }
+  }
+
   if (res.value != nullptr) {
     auto config_value = entry->FindOrCreateValue(res.config, res.product);
     if (!config_value->value) {
@@ -595,9 +667,9 @@
     } else {
       // When validation is enabled, ensure that a resource cannot have multiple values defined for
       // the same configuration unless protected by flags.
-      auto result =
-          validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), res.flag_status)
-                   : CollisionResult::kKeepBoth;
+      auto result = validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(),
+                                                    res.value->GetFlagStatus())
+                             : CollisionResult::kKeepBoth;
       if (result == CollisionResult::kConflict) {
         result = ResolveValueCollision(config_value->value.get(), res.value.get());
       }
@@ -771,11 +843,6 @@
   return *this;
 }
 
-NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) {
-  res_.flag_status = flag_status;
-  return *this;
-}
-
 NewResource NewResourceBuilder::Build() {
   return std::move(res_);
 }
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index cba6b70..b0e1855 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -136,6 +136,9 @@
   // The resource's values for each configuration.
   std::vector<std::unique_ptr<ResourceConfigValue>> values;
 
+  // The resource's values that are behind disabled flags.
+  std::vector<std::unique_ptr<ResourceConfigValue>> flag_disabled_values;
+
   explicit ResourceEntry(android::StringPiece name) : name(name) {
   }
 
@@ -148,6 +151,13 @@
                                          android::StringPiece product);
   std::vector<ResourceConfigValue*> FindAllValues(const android::ConfigDescription& config);
 
+  // Either returns the existing ResourceConfigValue in the disabled list with the given flag,
+  // config, and product or creates a new one and returns that. In either case the returned value
+  // does not have the flag set on the value so it must be set by the caller.
+  ResourceConfigValue* FindOrCreateFlagDisabledValue(const FeatureFlagAttribute& flag,
+                                                     const android::ConfigDescription& config,
+                                                     android::StringPiece product = {});
+
   template <typename Func>
   std::vector<ResourceConfigValue*> FindValuesIf(Func f) {
     std::vector<ResourceConfigValue*> results;
@@ -215,9 +225,14 @@
   std::optional<OverlayableItem> overlayable_item;
   std::optional<StagedId> staged_id;
   std::vector<const ResourceConfigValue*> values;
+  std::vector<const ResourceConfigValue*> flag_disabled_values;
 
   const ResourceConfigValue* FindValue(const android::ConfigDescription& config,
                                        android::StringPiece product = {}) const;
+
+  const ResourceConfigValue* FindFlagDisabledValue(const FeatureFlagAttribute& flag,
+                                                   const android::ConfigDescription& config,
+                                                   android::StringPiece product = {}) const;
 };
 
 struct ResourceTableTypeView {
@@ -269,7 +284,6 @@
   std::optional<AllowNew> allow_new;
   std::optional<StagedId> staged_id;
   bool allow_mangled = false;
-  FlagStatus flag_status = FlagStatus::NoFlag;
 };
 
 struct NewResourceBuilder {
@@ -283,7 +297,6 @@
   NewResourceBuilder& SetAllowNew(AllowNew allow_new);
   NewResourceBuilder& SetStagedId(StagedId id);
   NewResourceBuilder& SetAllowMangled(bool allow_mangled);
-  NewResourceBuilder& SetFlagStatus(FlagStatus flag_status);
   NewResource Build();
 
  private:
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index b75e87c..723cfc0 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -1102,6 +1102,7 @@
 std::unique_ptr<T> CopyValueFields(std::unique_ptr<T> new_value, const T* value) {
   new_value->SetSource(value->GetSource());
   new_value->SetComment(value->GetComment());
+  new_value->SetFlag(value->GetFlag());
   new_value->SetFlagStatus(value->GetFlagStatus());
   return new_value;
 }
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index a1b1839..e000c65 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -65,10 +65,21 @@
     return translatable_;
   }
 
+  void SetFlag(std::optional<FeatureFlagAttribute> val) {
+    flag_ = val;
+  }
+
+  std::optional<FeatureFlagAttribute> GetFlag() const {
+    return flag_;
+  }
+
   void SetFlagStatus(FlagStatus val) {
     flag_status_ = val;
   }
 
+  // If the value is behind a flag this returns whether that flag was enabled when the value was
+  // parsed by comparing it to the flags passed on the command line to aapt2 (taking into account
+  // negation if necessary). If there was no flag, FlagStatus::NoFlag is returned instead.
   FlagStatus GetFlagStatus() const {
     return flag_status_;
   }
@@ -128,6 +139,7 @@
   std::string comment_;
   bool weak_ = false;
   bool translatable_ = true;
+  std::optional<FeatureFlagAttribute> flag_;
   FlagStatus flag_status_ = FlagStatus::NoFlag;
 
  private:
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 5c64089..a0f60b6 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -240,6 +240,9 @@
 
   // The staged resource ID of this finalized resource.
   StagedId staged_id = 7;
+
+  // The set of values defined for this entry which are behind disabled flags
+  repeated ConfigValue flag_disabled_config_value = 8;
 }
 
 // A Configuration/Value pair.
@@ -283,6 +286,8 @@
 
   // The status of the flag the value is behind if any
   uint32 flag_status = 8;
+  bool flag_negated = 9;
+  string flag_name = 10;
 }
 
 // A CompoundValue is an abstract type. It represents a value that is a made of other values.
diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto
index b0ed3da3..f4735a2 100644
--- a/tools/aapt2/ResourcesInternal.proto
+++ b/tools/aapt2/ResourcesInternal.proto
@@ -49,4 +49,9 @@
 
   // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file).
   repeated Symbol exported_symbol = 5;
+
+  // The status of the flag the file is behind if any
+  uint32 flag_status = 6;
+  bool flag_negated = 7;
+  string flag_name = 8;
 }
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index 2a978a5..52372fa 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -67,6 +67,7 @@
   std::string resource_dir;
   std::string name;
   std::string extension;
+  std::string flag_name;
 
   // Original config str. We keep this because when we parse the config, we may add on
   // version qualifiers. We want to preserve the original input so the output is easily
@@ -81,6 +82,22 @@
                                                                std::string* out_error,
                                                                const CompileOptions& options) {
   std::vector<std::string> parts = util::Split(path, dir_sep);
+
+  std::string flag_name;
+  // Check for a flag
+  for (auto iter = parts.begin(); iter != parts.end();) {
+    if (iter->starts_with("flag(") && iter->ends_with(")")) {
+      if (!flag_name.empty()) {
+        if (out_error) *out_error = "resource path cannot contain more than one flag directory";
+        return {};
+      }
+      flag_name = iter->substr(5, iter->size() - 6);
+      iter = parts.erase(iter);
+    } else {
+      ++iter;
+    }
+  }
+
   if (parts.size() < 2) {
     if (out_error) *out_error = "bad resource path";
     return {};
@@ -131,6 +148,7 @@
                           std::string(dir_str),
                           std::string(name),
                           std::string(extension),
+                          std::move(flag_name),
                           std::string(config_str),
                           config};
 }
@@ -142,6 +160,9 @@
     name << "-" << data.config_str;
   }
   name << "_" << data.name;
+  if (!data.flag_name.empty()) {
+    name << ".(" << data.flag_name << ")";
+  }
   if (!data.extension.empty()) {
     name << "." << data.extension;
   }
@@ -163,7 +184,6 @@
                                        << "failed to open file: " << fin->GetError());
       return false;
     }
-
     // Parse the values file from XML.
     xml::XmlPullParser xml_parser(fin.get());
 
@@ -176,6 +196,18 @@
     // If visibility was forced, we need to use it when creating a new resource and also error if
     // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags.
     parser_options.visibility = options.visibility;
+    parser_options.flag = ParseFlag(path_data.flag_name);
+
+    if (parser_options.flag) {
+      std::string error;
+      auto flag_status = GetFlagStatus(parser_options.flag, options.feature_flag_values, &error);
+      if (flag_status) {
+        parser_options.flag_status = std::move(flag_status.value());
+      } else {
+        context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+        return false;
+      }
+    }
 
     ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config,
         parser_options);
@@ -402,6 +434,18 @@
   xmlres->file.config = path_data.config;
   xmlres->file.source = path_data.source;
   xmlres->file.type = ResourceFile::Type::kProtoXml;
+  xmlres->file.flag = ParseFlag(path_data.flag_name);
+
+  if (xmlres->file.flag) {
+    std::string error;
+    auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error);
+    if (flag_status) {
+      xmlres->file.flag_status = flag_status.value();
+    } else {
+      context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+      return false;
+    }
+  }
 
   // Collect IDs that are defined here.
   XmlIdCollector collector;
@@ -491,6 +535,27 @@
   res_file.source = path_data.source;
   res_file.type = ResourceFile::Type::kPng;
 
+  if (!path_data.flag_name.empty()) {
+    FeatureFlagAttribute flag;
+    auto name = path_data.flag_name;
+    if (name.starts_with('!')) {
+      flag.negated = true;
+      flag.name = name.substr(1);
+    } else {
+      flag.name = name;
+    }
+    res_file.flag = flag;
+
+    std::string error;
+    auto flag_status = GetFlagStatus(flag, options.feature_flag_values, &error);
+    if (flag_status) {
+      res_file.flag_status = flag_status.value();
+    } else {
+      context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+      return false;
+    }
+  }
+
   {
     auto data = file->OpenAsData();
     if (!data) {
diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp
index 6da3176..d3750a6 100644
--- a/tools/aapt2/cmd/Diff.cpp
+++ b/tools/aapt2/cmd/Diff.cpp
@@ -138,6 +138,22 @@
     }
   }
 
+  for (const ResourceConfigValue* config_value_a : entry_a.flag_disabled_values) {
+    auto config_value_b = entry_b.FindFlagDisabledValue(config_value_a->value->GetFlag().value(),
+                                                        config_value_a->config);
+    if (!config_value_b) {
+      std::stringstream str_stream;
+      str_stream << "missing disabled value " << pkg_a.name << ":" << type_a.named_type << "/"
+                 << entry_a.name << " config=" << config_value_a->config
+                 << " flag=" << config_value_a->value->GetFlag()->ToString();
+      EmitDiffLine(apk_b->GetSource(), str_stream.str());
+      diff = true;
+    } else {
+      diff |= EmitResourceConfigValueDiff(context, apk_a, pkg_a, type_a, entry_a, config_value_a,
+                                          apk_b, pkg_b, type_b, entry_b, config_value_b);
+    }
+  }
+
   // Check for any newly added config values.
   for (const ResourceConfigValue* config_value_b : entry_b.values) {
     auto config_value_a = entry_a.FindValue(config_value_b->config);
@@ -149,6 +165,18 @@
       diff = true;
     }
   }
+  for (const ResourceConfigValue* config_value_b : entry_b.flag_disabled_values) {
+    auto config_value_a = entry_a.FindFlagDisabledValue(config_value_b->value->GetFlag().value(),
+                                                        config_value_b->config);
+    if (!config_value_a) {
+      std::stringstream str_stream;
+      str_stream << "new disabled config " << pkg_b.name << ":" << type_b.named_type << "/"
+                 << entry_b.name << " config=" << config_value_b->config
+                 << " flag=" << config_value_b->value->GetFlag()->ToString();
+      EmitDiffLine(apk_b->GetSource(), str_stream.str());
+      diff = true;
+    }
+  }
   return diff;
 }
 
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index 7739171..08f8f0d 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -34,6 +34,44 @@
 
 namespace aapt {
 
+std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text) {
+  if (!flag_text || flag_text->empty()) {
+    return {};
+  }
+  FeatureFlagAttribute flag;
+  if (flag_text->starts_with('!')) {
+    flag.negated = true;
+    flag.name = flag_text->substr(1);
+  } else {
+    flag.name = flag_text.value();
+  }
+  return flag;
+}
+
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+                                        const FeatureFlagValues& feature_flag_values,
+                                        std::string* out_err) {
+  if (!flag) {
+    return FlagStatus::NoFlag;
+  }
+  auto flag_it = feature_flag_values.find(flag->name);
+  if (flag_it == feature_flag_values.end()) {
+    *out_err = "Resource flag value undefined: " + flag->name;
+    return {};
+  }
+  const auto& flag_properties = flag_it->second;
+  if (!flag_properties.read_only) {
+    *out_err = "Only read only flags may be used with resources: " + flag->name;
+    return {};
+  }
+  if (!flag_properties.enabled.has_value()) {
+    *out_err = "Only flags with a value may be used with resources: " + flag->name;
+    return {};
+  }
+  return (flag_properties.enabled.value() != flag->negated) ? FlagStatus::Enabled
+                                                            : FlagStatus::Disabled;
+}
+
 std::optional<uint16_t> ParseTargetDensityParameter(StringPiece arg, android::IDiagnostics* diag) {
   ConfigDescription preferred_density_config;
   if (!ConfigDescription::Parse(arg, &preferred_density_config)) {
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 6b8813b..d32e532 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -49,6 +49,12 @@
 
 using FeatureFlagValues = std::map<std::string, FeatureFlagProperties, std::less<>>;
 
+std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text);
+
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+                                        const FeatureFlagValues& feature_flag_values,
+                                        std::string* out_err);
+
 // Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc).
 // Returns Nothing and logs a human friendly error message if the string was not legal.
 std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg,
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 55f5e56..8583cad 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -536,6 +536,34 @@
 
         config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
                                                      &out_table->string_pool, files, out_error);
+
+        if (config_value->value == nullptr) {
+          return false;
+        }
+      }
+
+      // flag disabled
+      for (const pb::ConfigValue& pb_config_value : pb_entry.flag_disabled_config_value()) {
+        const pb::Configuration& pb_config = pb_config_value.config();
+
+        ConfigDescription config;
+        if (!DeserializeConfigFromPb(pb_config, &config, out_error)) {
+          return false;
+        }
+
+        FeatureFlagAttribute flag;
+        flag.name = pb_config_value.value().item().flag_name();
+        flag.negated = pb_config_value.value().item().flag_negated();
+        ResourceConfigValue* config_value =
+            entry->FindOrCreateFlagDisabledValue(std::move(flag), config, pb_config.product());
+        if (config_value->value != nullptr) {
+          *out_error = "duplicate configuration in resource table";
+          return false;
+        }
+
+        config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
+                                                     &out_table->string_pool, files, out_error);
+
         if (config_value->value == nullptr) {
           return false;
         }
@@ -615,6 +643,12 @@
   out_file->source.path = pb_file.source_path();
   out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
 
+  out_file->flag_status = (FlagStatus)pb_file.flag_status();
+  if (!pb_file.flag_name().empty()) {
+    out_file->flag =
+        FeatureFlagAttribute{.name = pb_file.flag_name(), .negated = pb_file.flag_negated()};
+  }
+
   std::string config_error;
   if (!DeserializeConfigFromPb(pb_file.config(), &out_file->config, &config_error)) {
     std::ostringstream error;
@@ -748,7 +782,6 @@
     if (value == nullptr) {
       return {};
     }
-
   } else if (pb_value.has_compound_value()) {
     const pb::CompoundValue& pb_compound_value = pb_value.compound_value();
     switch (pb_compound_value.value_case()) {
@@ -1018,6 +1051,12 @@
       DeserializeItemFromPbInternal(pb_item, src_pool, config, value_pool, files, out_error);
   if (item) {
     item->SetFlagStatus((FlagStatus)pb_item.flag_status());
+    if (!pb_item.flag_name().empty()) {
+      FeatureFlagAttribute flag;
+      flag.name = pb_item.flag_name();
+      flag.negated = pb_item.flag_negated();
+      item->SetFlag(std::move(flag));
+    }
   }
   return item;
 }
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 5772b3b..d83fe91 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -427,6 +427,14 @@
           SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
                              source_pool.get());
         }
+
+        for (const ResourceConfigValue* config_value : entry.flag_disabled_values) {
+          pb::ConfigValue* pb_config_value = pb_entry->add_flag_disabled_config_value();
+          SerializeConfig(config_value->config, pb_config_value->mutable_config());
+          pb_config_value->mutable_config()->set_product(config_value->product);
+          SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
+                             source_pool.get());
+        }
       }
     }
   }
@@ -721,6 +729,11 @@
   }
   if (out_value->has_item()) {
     out_value->mutable_item()->set_flag_status((uint32_t)value.GetFlagStatus());
+    if (value.GetFlag()) {
+      const auto& flag = value.GetFlag();
+      out_value->mutable_item()->set_flag_negated(flag->negated);
+      out_value->mutable_item()->set_flag_name(flag->name);
+    }
   }
 }
 
@@ -730,6 +743,11 @@
   item.Accept(&serializer);
   out_item->MergeFrom(value.item());
   out_item->set_flag_status((uint32_t)item.GetFlagStatus());
+  if (item.GetFlag()) {
+    const auto& flag = item.GetFlag();
+    out_item->set_flag_negated(flag->negated);
+    out_item->set_flag_name(flag->name);
+  }
 }
 
 void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) {
@@ -737,6 +755,11 @@
   out_file->set_source_path(file.source.path);
   out_file->set_type(SerializeFileReferenceTypeToPb(file.type));
   SerializeConfig(file.config, out_file->mutable_config());
+  out_file->set_flag_status((uint32_t)file.flag_status);
+  if (file.flag) {
+    out_file->set_flag_negated(file.flag->negated);
+    out_file->set_flag_name(file.flag->name);
+  }
 
   for (const SourcedResourceName& exported : file.exported_symbols) {
     pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
index c456e5c..7160b35 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
@@ -31,13 +31,29 @@
         "res/values/ints.xml",
         "res/values/strings.xml",
         "res/layout/layout1.xml",
+        "res/layout/layout3.xml",
+        "res/flag(test.package.falseFlag)/values/bools.xml",
+        "res/flag(test.package.falseFlag)/layout/layout2.xml",
+        "res/flag(test.package.falseFlag)/drawable/removedpng.png",
+        "res/flag(test.package.trueFlag)/layout/layout3.xml",
+        "res/values/flag(test.package.trueFlag)/bools.xml",
+        "res/values/flag(!test.package.trueFlag)/bools.xml",
+        "res/values/flag(!test.package.falseFlag)/bools.xml",
     ],
     out: [
+        "drawable_removedpng.(test.package.falseFlag).png.flat",
         "values_bools.arsc.flat",
+        "values_bools.(test.package.falseFlag).arsc.flat",
+        "values_bools.(test.package.trueFlag).arsc.flat",
+        "values_bools.(!test.package.falseFlag).arsc.flat",
+        "values_bools.(!test.package.trueFlag).arsc.flat",
         "values_bools2.arsc.flat",
         "values_ints.arsc.flat",
         "values_strings.arsc.flat",
         "layout_layout1.xml.flat",
+        "layout_layout2.(test.package.falseFlag).xml.flat",
+        "layout_layout3.xml.flat",
+        "layout_layout3.(test.package.trueFlag).xml.flat",
     ],
     cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
         "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png"
new file mode 100644
index 0000000..8a9e698
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png"
Binary files differ
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml"
new file mode 100644
index 0000000..dec5de7
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml"
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+</LinearLayout>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml"
new file mode 100644
index 0000000..c46c4d4
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="bool7">false</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml"
new file mode 100644
index 0000000..5aeee0e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml"
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <TextView android:id="@+id/text1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="foobar" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml
new file mode 100644
index 0000000..dec5de7
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 1ed0c8a..35975ed 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -9,4 +9,15 @@
     <bool name="bool3">false</bool>
 
     <bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool>
+
+    <bool name="bool5">false</bool>
+    <bool name="bool5" android:featureFlag="!test.package.falseFlag">true</bool>
+
+    <bool name="bool6">true</bool>
+    <bool name="bool6" android:featureFlag="!test.package.trueFlag">false</bool>
+
+    <bool name="bool7">true</bool>
+    <bool name="bool8">false</bool>
+    <bool name="bool9">true</bool>
+    <bool name="bool10">false</bool>
 </resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml"
new file mode 100644
index 0000000..a63749c
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="bool10">true</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml"
new file mode 100644
index 0000000..bb5526e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="bool9">false</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml"
new file mode 100644
index 0000000..eba780e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="bool8">true</bool>
+</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 3db37c2..6293008 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -17,6 +17,7 @@
 #include "LoadedApk.h"
 #include "cmd/Dump.h"
 #include "io/StringStream.h"
+#include "test/Common.h"
 #include "test/Test.h"
 #include "text/Printer.h"
 
@@ -75,6 +76,10 @@
 
   std::string output;
   DumpResourceTableToString(loaded_apk.get(), &output);
+  ASSERT_EQ(output.find("bool4"), std::string::npos);
+  ASSERT_EQ(output.find("str1"), std::string::npos);
+  ASSERT_EQ(output.find("layout2"), std::string::npos);
+  ASSERT_EQ(output.find("removedpng"), std::string::npos);
 }
 
 TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) {
@@ -86,6 +91,8 @@
 
   ASSERT_EQ(output.find("bool4"), std::string::npos);
   ASSERT_EQ(output.find("str1"), std::string::npos);
+  ASSERT_EQ(output.find("layout2"), std::string::npos);
+  ASSERT_EQ(output.find("removedpng"), std::string::npos);
 }
 
 TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) {
@@ -98,4 +105,47 @@
   ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
 }
 
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlag) {
+  test::TestDiagnosticsImpl diag;
+  const std::string compiled_files_dir = GetTestPath("compiled");
+  ASSERT_FALSE(CompileFile(
+      GetTestPath("res/values/values.xml"),
+      R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+           <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+           <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+         </resources>)",
+      compiled_files_dir, &diag,
+      {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+  ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool/bool1'"));
+}
+
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) {
+  test::TestDiagnosticsImpl diag;
+  const std::string compiled_files_dir = GetTestPath("compiled");
+  ASSERT_TRUE(CompileFile(
+      GetTestPath("res/values/values1.xml"),
+      R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+           <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+         </resources>)",
+      compiled_files_dir, &diag,
+      {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+  ASSERT_TRUE(CompileFile(
+      GetTestPath("res/values/values2.xml"),
+      R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+           <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+         </resources>)",
+      compiled_files_dir, &diag,
+      {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+  const std::string out_apk = GetTestPath("out.apk");
+  std::vector<std::string> link_args = {
+      "--manifest",
+      GetDefaultManifest(),
+      "-o",
+      out_apk,
+  };
+
+  ASSERT_FALSE(Link(link_args, compiled_files_dir, &diag));
+  ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'"));
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 37a039e..1bef5f8 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -321,6 +321,30 @@
           }
         }
       }
+
+      // disabled values
+      for (auto& src_config_value : src_entry->flag_disabled_values) {
+        auto dst_config_value = dst_entry->FindOrCreateFlagDisabledValue(
+            src_config_value->value->GetFlag().value(), src_config_value->config,
+            src_config_value->product);
+        if (!dst_config_value->value) {
+          // Resource does not exist, add it now.
+          // Must clone the value since it might be in the values vector as well
+          CloningValueTransformer cloner(&main_table_->string_pool);
+          dst_config_value->value = src_config_value->value->Transform(cloner);
+        } else {
+          error = true;
+          context_->GetDiagnostics()->Error(
+              android::DiagMessage(src_config_value->value->GetSource())
+              << "duplicate value for resource '" << src_entry->name << "' " << "with config '"
+              << src_config_value->config << "' and flag '"
+              << (src_config_value->value->GetFlag()->negated ? "!" : "")
+              << src_config_value->value->GetFlag()->name << "'");
+          context_->GetDiagnostics()->Note(
+              android::DiagMessage(dst_config_value->value->GetSource())
+              << "resource previously defined here");
+        }
+      }
     }
   }
   return !error;
@@ -353,6 +377,8 @@
   file_ref->SetSource(file_desc.source);
   file_ref->type = file_desc.type;
   file_ref->file = file;
+  file_ref->SetFlagStatus(file_desc.flag_status);
+  file_ref->SetFlag(file_desc.flag);
 
   ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package);
   pkg->FindOrCreateType(file_desc.name.type)
diff --git a/tools/aapt2/test/Common.cpp b/tools/aapt2/test/Common.cpp
index cdf24534..c7dd4c9 100644
--- a/tools/aapt2/test/Common.cpp
+++ b/tools/aapt2/test/Common.cpp
@@ -21,23 +21,6 @@
 namespace aapt {
 namespace test {
 
-struct TestDiagnosticsImpl : public android::IDiagnostics {
-  void Log(Level level, android::DiagMessageActual& actual_msg) override {
-    switch (level) {
-      case Level::Note:
-        return;
-
-      case Level::Warn:
-        std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
-        break;
-
-      case Level::Error:
-        std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
-        break;
-    }
-  }
-};
-
 android::IDiagnostics* GetDiagnostics() {
   static TestDiagnosticsImpl diag;
   return &diag;
diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h
index 0437980..b06c432 100644
--- a/tools/aapt2/test/Common.h
+++ b/tools/aapt2/test/Common.h
@@ -37,6 +37,32 @@
 namespace aapt {
 namespace test {
 
+struct TestDiagnosticsImpl : public android::IDiagnostics {
+  void Log(Level level, android::DiagMessageActual& actual_msg) override {
+    switch (level) {
+      case Level::Note:
+        return;
+
+      case Level::Warn:
+        std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+        log << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+        break;
+
+      case Level::Error:
+        std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+        log << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+        break;
+    }
+  }
+
+  std::string GetLog() {
+    return log.str();
+  }
+
+ private:
+  std::ostringstream log;
+};
+
 android::IDiagnostics* GetDiagnostics();
 
 inline ResourceName ParseNameOrDie(android::StringPiece str) {
diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp
index b91abe5..570bcf1 100644
--- a/tools/aapt2/test/Fixture.cpp
+++ b/tools/aapt2/test/Fixture.cpp
@@ -91,10 +91,13 @@
 }
 
 bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents,
-                                     android::StringPiece out_dir, android::IDiagnostics* diag) {
+                                     android::StringPiece out_dir, android::IDiagnostics* diag,
+                                     const std::vector<android::StringPiece>& additional_args) {
   WriteFile(path, contents);
   CHECK(file::mkdirs(out_dir.data()));
-  return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0;
+  std::vector<android::StringPiece> args = {path, "-o", out_dir, "-v"};
+  args.insert(args.end(), additional_args.begin(), additional_args.end());
+  return CompileCommand(diag).Execute(args, &std::cerr) == 0;
 }
 
 bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) {
diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h
index 14298d16..178d011 100644
--- a/tools/aapt2/test/Fixture.h
+++ b/tools/aapt2/test/Fixture.h
@@ -73,7 +73,8 @@
   // Wries the contents of the file to the specified path. The file is compiled and the flattened
   // file is written to the out directory.
   bool CompileFile(const std::string& path, const std::string& contents,
-                   android::StringPiece flat_out_dir, android::IDiagnostics* diag);
+                   android::StringPiece flat_out_dir, android::IDiagnostics* diag,
+                   const std::vector<android::StringPiece>& additional_args = {});
 
   // Executes the link command with the specified arguments.
   bool Link(const std::vector<std::string>& args, android::IDiagnostics* diag);